package main import ( "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "strings" "time" ) const TWITTER_API_URL = "https://api.twitter.com/1.1" type TwitterApi struct { BaseUrl string AccessToken string } func NewTwitterApi(baseUrl, accesToken string) *TwitterApi { return &TwitterApi{baseUrl, accesToken} } func (t *TwitterApi) encodeParams(params map[string]string) string { q := url.Values{} for k, v := range params { q.Set(k, v) } return q.Encode() } func (t *TwitterApi) createGetPathAndParams(apiEndpoint string, params map[string]string) string { u, err := url.Parse(apiEndpoint) if err != nil { panic(err) } q := u.Query() for k, v := range params { q.Set(k, v) } u.RawQuery = q.Encode() return u.String() } func (t *TwitterApi) GetBase64EncodedBearerTokenCredentials(consumerKey, consumerSecret string) string { data := []byte(consumerKey + ":" + consumerSecret) return base64.URLEncoding.EncodeToString(data) } func (t *TwitterApi) Get(path_ string) (resp *http.Response, err error) { req, err := http.NewRequest("GET", t.BaseUrl+path_, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+t.AccessToken) req.Header.Set("content-type", "application/json; charset=utf-8") return http.DefaultClient.Do(req) } func (t *TwitterApi) Post(path_ string, body string) (resp *http.Response, err error) { req, err := http.NewRequest("POST", t.BaseUrl+path_, bytes.NewBufferString(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+t.AccessToken) req.Header.Set("content-type", "application/json; charset=utf-8") return http.DefaultClient.Do(req) } func (t *TwitterApi) GetAndDeserialize(path string, params map[string]string, v interface{}) (err error) { defer func() { if err == io.EOF { err = nil } }() if r, err := t.Get(t.createGetPathAndParams(path, params)); err != nil { return err } else if r.StatusCode/100 != 2 { if errMsg, err := ioutil.ReadAll(r.Body); err != nil { return errors.New("error getting endpoint and cannot deserialize error message") } else { return NewTwitterErr(string(errMsg), r.StatusCode) } } else { d := json.NewDecoder(r.Body) return d.Decode(v) } } func (t *TwitterApi) PostAndDeserialize(path string, params map[string]string, v interface{}) (err error) { defer func() { if err == io.EOF { err = nil } }() if r, err := t.Post(path, t.encodeParams(params)); err != nil { return err } else if r.StatusCode/100 != 2 { if errMsg, err := ioutil.ReadAll(r.Body); err != nil { return errors.New("error getting endpoint and cannot deserialize error message") } else { return NewTwitterErr(string(errMsg), r.StatusCode) } } else { d := json.NewDecoder(r.Body) return d.Decode(v) } } func (t *TwitterApi) GetTwitterIdByScreenName(sceenName string) (id uint64, err error) { ids := make([]*idHolder, 0, 1) params := map[string]string{"screen_name": sceenName, "include_entities": "id"} apiPath := "/users/lookup.json" if err := t.GetAndDeserialize(apiPath, params, &ids); err != nil { return 0, err } else if len(ids) < 1 { return 0, errors.New("cannot find user with screen name " + sceenName) } return ids[0].Id, nil } func (t *TwitterApi) GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList { followerListC := make(chan *FollowerList) go func() { params := map[string]string{"screen_name": screenName, "count": "200", "skip_status": "true", "cursor": cursor} apiPath := "/followers/list.json" followers := new(FollowerList) if err := t.GetAndDeserialize(apiPath, params, followers); err == nil { followerListC <- followers } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { log.Println("api limit reached, need to sleep") time.Sleep(5 * 60 * time.Second) followerListC <- <-t.GetFollowerByCursor(screenName, cursor) } else { log.Println(err) followerListC <- nil } }() return followerListC } func (t *TwitterApi) GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList { followerListC := make(chan *FollowerIDList) go func() { params := map[string]string{"screen_name": screenName, "count": "5000", "cursor": cursor} apiPath := "/followers/ids.json" followers := new(FollowerIDList) if err := t.GetAndDeserialize(apiPath, params, followers); err == nil { followerListC <- followers } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { log.Println("api limit reached, need to sleep") time.Sleep(5 * 60 * time.Second) followerListC <- <-t.GetGetFollowerIdsByCursor(screenName, cursor) } else { log.Println(err) followerListC <- nil } }() return followerListC } func (t *TwitterApi) GetScreenNameOfUsersByIds(ids []uint64) <-chan string { if len(ids) >= 100 { log.Println("GetScreenNameOfUsersByIds received a list of more that 100 ids. This is not supported by twitter") } screenNameC := make(chan string) go func() { path := "/users/lookup.json" params := map[string]string{"include_entities": "false", "user_id": t.asCommaSeparatedString(ids)} users := make([]*User, 0, len(ids)) if err := t.PostAndDeserialize(path, params, users); err == nil { for _, user := range users { screenNameC <- user.ScreenName } } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { log.Println("api limit reached, need to sleep") time.Sleep(5 * 60 * time.Second) screenNameC <- <-t.GetScreenNameOfUsersByIds(ids) } else { log.Println(err) screenNameC <- "" } close(screenNameC) }() return screenNameC } func (t *TwitterApi) asCommaSeparatedString(lst []uint64) string { lstAsString := make([]string, len(lst)) for i, n := range lst { lstAsString[i] = fmt.Sprintf("%v", n) } return strings.Join(lstAsString, ",") }