diff --git a/README b/README index 47200eb..fe602ab 100644 --- a/README +++ b/README @@ -41,36 +41,6 @@ result, err := api.GetSearch("golang", v) (Remember that `url.Values` is equivalent to a `map[string][]string`, if you find that more convenient notation when specifying values). Otherwise, `nil` suffices. -### Streaming - -Anaconda supports the Streaming APIs. You can use `PublicStream*` or `UserStream` API methods. -A go loop is started an gives you an stream that sends `interface{}` objects through it's `chan` `C` -Objects which you can cast into a tweet, event and more. - - -````go -v := url.Values{} -s := api.UserStream(v) - -for t := range s.C { - switch v := t.(type) { - case anaconda.Tweet: - fmt.Printf("%-15s: %s\n", v.User.ScreenName, v.Text) - case anaconda.EventTweet: - switch v.Event.Event { - case "favorite": - sn := v.Source.ScreenName - tw := v.TargetObject.Text - fmt.Printf("Favorited by %-15s: %s\n", sn, tw) - case "unfavorite": - sn := v.Source.ScreenName - tw := v.TargetObject.Text - fmt.Printf("UnFavorited by %-15s: %s\n", sn, tw) - } - } -} -```` - Endpoints diff --git a/twitter.go b/twitter.go index 069d581..1777f70 100644 --- a/twitter.go +++ b/twitter.go @@ -31,7 +31,7 @@ // //Endpoints // -//Anaconda implements most of the endpoints defined in the Twitter API documentation: https://dev.twitter.com/docs/api/1.1. +//Anaconda implements most of the endpoints defined in the Twitter API documentation: https://developer.twitter.com/en/docs //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`). // //In a few cases, a shortened form has been chosen to make life easier (for example, retweeting is simply the function `Retweet`) @@ -40,6 +40,7 @@ package anaconda import ( + "bytes" "compress/zlib" "encoding/json" "fmt" @@ -58,6 +59,7 @@ const ( _POST = iota _DELETE = iota _PUT = iota + _GETBEARER = iota ClientTimeout = 20 BaseUrlV1 = "https://api.twitter.com/1" BaseUrl = "https://api.twitter.com/1.1" @@ -71,6 +73,7 @@ var ( type TwitterApi struct { oauthClient oauth.Client Credentials *oauth.Credentials + bearer string queryQueue chan query bucket *tokenbucket.Bucket returnRateLimitError bool @@ -84,6 +87,13 @@ type TwitterApi struct { // used for testing // defaults to BaseUrl baseUrl string + + // environment name used by Premium Account Activity API + // Leave nil for Enterprise Account Activity API + env string + + // Prefix used for Account Activity API. + activityUrl string } type query struct { @@ -125,6 +135,8 @@ func NewTwitterApi(access_token string, access_token_secret string) *TwitterApi HttpClient: http.DefaultClient, Log: silentLogger{}, baseUrl: BaseUrl, + env: "", + activityUrl: BaseUrl + "/account_activity/", } //Configure a timeout to HTTP client (DefaultClient has no default timeout, which may deadlock Mutex-wrapped uses of the lib.) c.HttpClient.Timeout = time.Duration(ClientTimeout * time.Second) @@ -142,13 +154,13 @@ func NewTwitterApiWithCredentials(access_token string, access_token_secret strin } //SetConsumerKey will set the application-specific consumer_key used in the initial OAuth process -//This key is listed on https://dev.twitter.com/apps/YOUR_APP_ID/show +//This key is listed on https://developer.twitter.com/en/apps/YOUR_APP_ID/show func SetConsumerKey(consumer_key string) { oauthCredentials.Token = consumer_key } //SetConsumerSecret will set the application-specific secret used in the initial OAuth process -//This secret is listed on https://dev.twitter.com/apps/YOUR_APP_ID/show +//This secret is listed on https://deeloperv.twitter.com/en/apps/YOUR_APP_ID/show func SetConsumerSecret(consumer_secret string) { oauthCredentials.Secret = consumer_secret } @@ -183,6 +195,16 @@ func (c *TwitterApi) GetDelay() time.Duration { // SetBaseUrl is experimental and may be removed in future releases. func (c *TwitterApi) SetBaseUrl(baseUrl string) { c.baseUrl = baseUrl + c.SetEnv(c.env) //propagate to activityUrl +} + +// SetEnv sets the environment name used by Premium Account Activity API. +func (c *TwitterApi) SetEnv(env string) { + c.env = env + c.activityUrl = c.baseUrl + "/account_activity/" + if env != "" { + c.activityUrl = c.activityUrl + "all/" + env + "/" + } } //AuthorizationURL generates the authorization URL for the first part of the OAuth handshake. @@ -259,6 +281,41 @@ func (c TwitterApi) apiPut(urlStr string, form url.Values, data interface{}) err return decodeResponse(resp, data) } +// apiGetBearer issues a GET request with a bearer token. Done outside the oauth library because +// it doesn't support bearer tokens currently. +func (c TwitterApi) apiGetBearer(urlStr string, form url.Values, data interface{}) error { + // form is ignored + req, _ := http.NewRequest("GET", urlStr, nil) + if c.bearer == "" { + c.bearer, _ = c.GetBearerToken() + } + + req.Header.Add("Authorization", "Bearer "+c.bearer) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + return decodeResponse(resp, data) +} + +func (a TwitterApi) GetBearerToken() (tok string, err error) { + var bt BearerToken + client := &http.Client{} + req, err := http.NewRequest("POST", "https://api.twitter.com/oauth2/token", bytes.NewReader([]byte("grant_type=client_credentials"))) + req.SetBasicAuth(a.oauthClient.Credentials.Token, a.oauthClient.Credentials.Secret) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return "", err + } + bodyText, err := ioutil.ReadAll(resp.Body) + err = json.Unmarshal(bodyText, &bt) + + return bt.Token, err //Note that it is still in URL encoded form +} + // decodeResponse decodes the JSON response from the Twitter API. func decodeResponse(resp *http.Response, data interface{}) error { // Prevent memory leak in the case where the Response.Body is not used. @@ -279,7 +336,8 @@ func decodeResponse(resp *http.Response, data interface{}) error { // according to dev.twitter.com, chunked upload append returns HTTP 2XX // so we need a special case when decoding the response if strings.HasSuffix(resp.Request.URL.String(), "upload.json") || - strings.Contains(resp.Request.URL.String(), "webhooks") { + strings.Contains(resp.Request.URL.String(), "webhooks") || + strings.Contains(resp.Request.URL.String(), "subscriptions") { if resp.StatusCode == 204 { // empty response, don't decode return nil @@ -316,6 +374,8 @@ func (c TwitterApi) execQuery(urlStr string, form url.Values, data interface{}, return c.apiDel(urlStr, form, data) case _PUT: return c.apiPut(urlStr, form, data) + case _GETBEARER: + return c.apiGetBearer(urlStr, form, data) default: return fmt.Errorf("HTTP method not yet supported") } diff --git a/webhook.go b/webhook.go index 1dca990..bff9b49 100644 --- a/webhook.go +++ b/webhook.go @@ -1,17 +1,22 @@ package anaconda import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" "net/url" ) //GetActivityWebhooks represents the twitter account_activity webhook //Returns all URLs and their statuses for the given app. Currently, -//only one webhook URL can be registered to an application. -//https://dev.twitter.com/webhooks/reference/get/account_activity/webhooks +//only one webhook URL can be registered to an application except in the Enterprise API +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#get-account-activity-all-webhooks func (a TwitterApi) GetActivityWebhooks(v url.Values) (u []WebHookResp, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks.json", v, &u, _GET, responseCh} + a.queryQueue <- query{a.activityUrl + "webhooks.json", v, &u, _GET, responseCh} return u, (<-responseCh).err } @@ -23,63 +28,111 @@ type WebHookResp struct { CreatedAt string } +type BearerToken struct { + Type string `json:"token_type"` + Token string `json:"access_token"` +} + //SetActivityWebhooks represents to set twitter account_activity webhook //Registers a new webhook URL for the given application context. //The URL will be validated via CRC request before saving. In case the validation fails, -//a comprehensive error is returned. message to the requester. -//Only one webhook URL can be registered to an application. -//https://api.twitter.com/1.1/account_activity/webhooks.json +//a comprehensive error message is returned to the requester. +//Only one webhook URL can be registered to an application except in the Enterprise API. +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#post-account-activity-all-env-name-webhooks func (a TwitterApi) SetActivityWebhooks(v url.Values) (u WebHookResp, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks.json", v, &u, _POST, responseCh} + a.queryQueue <- query{a.activityUrl + "webhooks.json", v, &u, _POST, responseCh} return u, (<-responseCh).err } -//DeleteActivityWebhooks Removes the webhook from the provided application’s configuration. -//https://dev.twitter.com/webhooks/reference/del/account_activity/webhooks +//DeleteActivityWebhooks Removes a webhook from the provided application’s configuration. +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#delete-account-activity-all-env-name-webhooks-webhook-id func (a TwitterApi) DeleteActivityWebhooks(v url.Values, webhookID string) (u interface{}, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks/" + webhookID + ".json", v, &u, _DELETE, responseCh} + a.queryQueue <- query{a.activityUrl + "webhooks/" + webhookID + ".json", v, &u, _DELETE, responseCh} return u, (<-responseCh).err } -//PutActivityWebhooks update webhook which reenables the webhook by setting its status to valid. -//https://dev.twitter.com/webhooks/reference/put/account_activity/webhooks +//PutActivityWebhooks Updates a webhook by triggering a challenge response check (CRC) which, if +//successful, sets its status to valid. +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#put-account-activity-all-env-name-webhooks-webhook-id func (a TwitterApi) PutActivityWebhooks(v url.Values, webhookID string) (u interface{}, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks/" + webhookID + ".json", v, &u, _PUT, responseCh} + a.queryQueue <- query{a.activityUrl + "webhooks/" + webhookID + ".json", v, &u, _PUT, responseCh} return u, (<-responseCh).err } //SetWHSubscription Subscribes the provided app to events for the provided user context. -//When subscribed, all DM events for the provided user will be sent to the app’s webhook via POST request. -//https://dev.twitter.com/webhooks/reference/post/account_activity/webhooks/subscriptions +//When subscribed, all events for the provided user will be sent to the app’s webhook via POST request. +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#post-account-activity-all-env-name-subscriptions func (a TwitterApi) SetWHSubscription(v url.Values, webhookID string) (u interface{}, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks/" + webhookID + "/subscriptions.json", v, &u, _POST, responseCh} + if a.env != "" { + a.queryQueue <- query{a.activityUrl + "subscriptions.json", v, &u, _POST, responseCh} + } else { + a.queryQueue <- query{a.activityUrl + "webhooks/" + webhookID + "/subscriptions/all.json", v, &u, _POST, responseCh} + } return u, (<-responseCh).err } -//GetWHSubscription Provides a way to determine if a webhook configuration is -//subscribed to the provided user’s Direct Messages. -//https://dev.twitter.com/webhooks/reference/get/account_activity/webhooks/subscriptions +//GetWHSubscription Determines if a webhook configuration is subscribed to the provided user’s account +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#get-account-activity-all-env-name-subscriptions func (a TwitterApi) GetWHSubscription(v url.Values, webhookID string) (u interface{}, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks/" + webhookID + "/subscriptions.json", v, &u, _GET, responseCh} + if a.env != "" { + a.queryQueue <- query{a.activityUrl + "subscriptions.json", v, &u, _GET, responseCh} + } else { + a.queryQueue <- query{a.activityUrl + "webhooks/" + webhookID + "/subscriptions/all.json", v, &u, _GET, responseCh} + } + return u, (<-responseCh).err +} + +//ListWHSubscriptions Returns a list of active subscriptions +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#get-account-activity-all-env-name-subscriptions-list +func (a TwitterApi) ListWHSubscriptions(v url.Values) (u interface{}, err error) { + v = cleanValues(v) + responseCh := make(chan response) + + a.queryQueue <- query{a.activityUrl + "subscriptions/list.json", v, &u, _GETBEARER, responseCh} return u, (<-responseCh).err } //DeleteWHSubscription Deactivates subscription for the provided user context and app. After deactivation, -//all DM events for the requesting user will no longer be sent to the webhook URL.. +//all events for the requesting user will no longer be sent to the webhook URL. //https://dev.twitter.com/webhooks/reference/del/account_activity/webhooks func (a TwitterApi) DeleteWHSubscription(v url.Values, webhookID string) (u interface{}, err error) { v = cleanValues(v) responseCh := make(chan response) - a.queryQueue <- query{a.baseUrl + "/account_activity/webhooks/" + webhookID + "/subscriptions.json", v, &u, _DELETE, responseCh} + if a.env != "" { + a.queryQueue <- query{a.activityUrl + "subscriptions.json", v, &u, _DELETE, responseCh} + } else { + a.queryQueue <- query{a.activityUrl + "webhooks/" + webhookID + "/subscriptions/all.json", v, &u, _DELETE, responseCh} + } return u, (<-responseCh).err } + +//CountWHSubscriptions returns the count of subscriptions active on a given webhook +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium#get-account-activity-all-subscriptions-count +func (a TwitterApi) CountWHSubscriptions(v url.Values) (u interface{}, err error) { + v = cleanValues(v) + responseCh := make(chan response) + //note lack of environment name here, even for Premium + + a.queryQueue <- query{a.baseUrl + "/account_activity/all/subscriptions/count.json", v, &u, _GETBEARER, responseCh} + return u, (<-responseCh).err +} + +//RespondCRC responds to a CRC request from Twitter. +//Should be called in response to a GET request to the callback URL. +//https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks +func (a TwitterApi) RespondCRC(tok string, w http.ResponseWriter) { + mac := hmac.New(sha256.New, []byte(a.oauthClient.Credentials.Secret)) + mac.Write([]byte(tok)) + resp := "{ \"response_token\": \"sha256=" + base64.StdEncoding.EncodeToString(mac.Sum(nil)) + "\" }" + fmt.Fprint(w, resp) +}