Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account Activity API updates #261

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 64 additions & 4 deletions twitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -40,6 +40,7 @@
package anaconda

import (
"bytes"
"compress/zlib"
"encoding/json"
"fmt"
Expand All @@ -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"
Expand All @@ -71,6 +73,7 @@ var (
type TwitterApi struct {
oauthClient oauth.Client
Credentials *oauth.Credentials
bearer string
queryQueue chan query
bucket *tokenbucket.Bucket
returnRateLimitError bool
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down
97 changes: 75 additions & 22 deletions webhook.go
Original file line number Diff line number Diff line change
@@ -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
}

Expand All @@ -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)
}