first commit
This commit is contained in:
commit
a617cf50ee
8 files changed
+799
No files matched your search
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type idHolder struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ScreenName string `json:"screen_name"`
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FollowerList struct {
|
||||||
|
NextCursor string `json:"next_cursor_str"`
|
||||||
|
Followers []*User `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FollowerList) GetFollowerScreenNames() []string {
|
||||||
|
ret := make([]string, len(f.Followers))
|
||||||
|
for i, u := range f.Followers {
|
||||||
|
ret[i] = u.ScreenName
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type FollowerIDList struct {
|
||||||
|
NextCursor string `json:"next_cursor_str"`
|
||||||
|
Followers []uint64 `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwitterErr struct {
|
||||||
|
Msg string
|
||||||
|
Status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *TwitterErr) Error() string {
|
||||||
|
return err.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTwitterErr(msg string, status int) *TwitterErr {
|
||||||
|
return &TwitterErr{msg, status}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserJsonification(t *testing.T) {
|
||||||
|
jsonForm := `{"screen_name": "boblechef", "id": 2920819021}`
|
||||||
|
expectedUser := &User{"boblechef", 2920819021}
|
||||||
|
u := new(User)
|
||||||
|
if err := json.Unmarshal([]byte(jsonForm), u); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !reflect.DeepEqual(u, expectedUser) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowerListJsonification(t *testing.T) {
|
||||||
|
jsonForm := `{"next_cursor_str": "3333", "users":[ {"screen_name": "boblechef", "id": 2920819021}]}`
|
||||||
|
expectedUserList := &FollowerList{"3333", []*User{&User{"boblechef", 2920819021}}}
|
||||||
|
l := new(FollowerList)
|
||||||
|
if err := json.Unmarshal([]byte(jsonForm), l); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if !reflect.DeepEqual(l, expectedUserList) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// returns all the element x that are in c1 and c2. can retuns many time
|
||||||
|
// the same x.
|
||||||
|
func intersection(c1, c2 <-chan uint64) <-chan uint64 {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
out := make(chan uint64)
|
||||||
|
|
||||||
|
mc1, mc2 := make(map[uint64]bool), make(map[uint64]bool)
|
||||||
|
|
||||||
|
filter := func(c <-chan uint64, mapToAdd, mapToVerify map[uint64]bool) {
|
||||||
|
for n := range c {
|
||||||
|
mapToAdd[n] = true
|
||||||
|
if _, ok := mapToVerify[n]; ok {
|
||||||
|
out <- n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
wg.Add(2)
|
||||||
|
go filter(c1, mc1, mc2)
|
||||||
|
go filter(c2, mc2, mc1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(out)
|
||||||
|
}()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all duplicates in inputC
|
||||||
|
func uniq(inputC <-chan uint64) <-chan uint64 {
|
||||||
|
m := make(map[uint64]bool)
|
||||||
|
c := make(chan uint64)
|
||||||
|
go func() {
|
||||||
|
for n := range inputC {
|
||||||
|
if _, ok := m[n]; !ok {
|
||||||
|
m[n] = true
|
||||||
|
c <- n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(c)
|
||||||
|
}()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the intersection of a and b without repetitions
|
||||||
|
func Intersection(a, b <-chan uint64) <-chan uint64 {
|
||||||
|
return uniq(intersection(a, b))
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeUInt64Channel(numbers ...uint64) <-chan uint64 {
|
||||||
|
c := make(chan uint64)
|
||||||
|
go func() {
|
||||||
|
for _, n := range numbers {
|
||||||
|
c <- n
|
||||||
|
}
|
||||||
|
close(c)
|
||||||
|
}()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUInt64Channel(c <-chan uint64) []uint64 {
|
||||||
|
ret := make([]uint64, 0)
|
||||||
|
for n := range c {
|
||||||
|
ret = append(ret, n)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalsAsMultiSet(a, b []uint64) bool {
|
||||||
|
aSet, bSet := make(map[uint64]int), make(map[uint64]int)
|
||||||
|
for _, e := range a {
|
||||||
|
if _, ok := aSet[e]; ok {
|
||||||
|
aSet[e]++
|
||||||
|
} else {
|
||||||
|
aSet[e] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range b {
|
||||||
|
if _, ok := aSet[e]; ok {
|
||||||
|
bSet[e]++
|
||||||
|
} else {
|
||||||
|
bSet[e] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(aSet, bSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniq(t *testing.T) {
|
||||||
|
c := makeUInt64Channel(1, 3, 2, 1, 4, 3, 3, 2, 1)
|
||||||
|
expt := []uint64{1, 3, 2, 4}
|
||||||
|
if ret := readUInt64Channel(uniq(c)); !reflect.DeepEqual(expt, ret) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersection(t *testing.T) {
|
||||||
|
c := Intersection(makeUInt64Channel(1, 2, 3, 4), makeUInt64Channel(1, 2, 3, 4))
|
||||||
|
if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{1, 2, 3, 4}) {
|
||||||
|
t.Error("needed {1, 2, 3 , 4} received ", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = Intersection(makeUInt64Channel(1, 2), makeUInt64Channel(1, 2, 3, 4))
|
||||||
|
if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{1, 2}) {
|
||||||
|
t.Error("needed {1, 2} received ", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = Intersection(makeUInt64Channel(3, 4), makeUInt64Channel(1, 2, 3, 4))
|
||||||
|
if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{3, 4}) {
|
||||||
|
t.Error("needed {3 , 4} received ", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FollowerGetter interface {
|
||||||
|
GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList
|
||||||
|
GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList
|
||||||
|
GetScreenNameOfUsersByIds(ids []uint64) <-chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFollowerScreenNames(followerGetter FollowerGetter, screenName string) <-chan string {
|
||||||
|
followerC := make(chan string)
|
||||||
|
go func() {
|
||||||
|
nextCursor := "-1"
|
||||||
|
for nextCursor != "0" && nextCursor != "" {
|
||||||
|
followers := <-followerGetter.GetFollowerByCursor(screenName, nextCursor)
|
||||||
|
nextCursor = followers.NextCursor
|
||||||
|
for _, follower := range followers.GetFollowerScreenNames() {
|
||||||
|
followerC <- follower
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(followerC)
|
||||||
|
}()
|
||||||
|
return followerC
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFollowerIds(followerGetter FollowerGetter, screenName string) <-chan uint64 {
|
||||||
|
followerC := make(chan uint64)
|
||||||
|
go func() {
|
||||||
|
nextCursor := "-1"
|
||||||
|
for nextCursor != "0" && nextCursor != "" {
|
||||||
|
followers := <-followerGetter.GetGetFollowerIdsByCursor(screenName, nextCursor)
|
||||||
|
nextCursor = followers.NextCursor
|
||||||
|
for _, follower := range followers.Followers {
|
||||||
|
followerC <- follower
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(followerC)
|
||||||
|
}()
|
||||||
|
return followerC
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTwitterScreenNameByIds(followerGetter FollowerGetter, idsC <-chan uint64) <-chan string {
|
||||||
|
screenNameC := make(chan string)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
produceScreenName := func(buffer []uint64) {
|
||||||
|
for screenName := range followerGetter.GetScreenNameOfUsersByIds(buffer) {
|
||||||
|
screenNameC <- screenName
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buffer := make([]uint64, 0, 100)
|
||||||
|
for id := range idsC {
|
||||||
|
buffer = append(buffer, id)
|
||||||
|
if len(buffer) >= 95 {
|
||||||
|
wg.Add(1)
|
||||||
|
go produceScreenName(buffer)
|
||||||
|
buffer = make([]uint64, 0, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go produceScreenName(buffer)
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(screenNameC)
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
return screenNameC
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFollowerIdsOfBothAccounts(followerGetter FollowerGetter, screenName1, screenName2 string) <-chan uint64 {
|
||||||
|
return Intersection(GetFollowerIds(followerGetter, screenName1), GetFollowerIds(followerGetter, screenName1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
token := "AAAAAAAAAAAAAAAAAAAAAPwfcQAAAAAAzkou%2FHjJNJmwdepeRq0c%2Bi3Nx6o%3DXofLt7SVvc99ulETLRA3yS2lYo8smfc6tACxEYsLUmGsrNbc9J"
|
||||||
|
t := NewTwitterApi(TWITTER_API_URL, token)
|
||||||
|
ff := Intersection(GetFollowerIds(t, "Shopify"), GetFollowerIds(t, "ShopifyPicks"))
|
||||||
|
for screenName := range GetTwitterScreenNameByIds(t, ff) {
|
||||||
|
fmt.Println(screenName)
|
||||||
|
}
|
||||||
|
}
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
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, ",")
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestcreateGetPathAndParams(t *testing.T) {
|
||||||
|
tw := NewTwitterApi("twitter.com", "access_token")
|
||||||
|
p := map[string]string{"hello": "world"}
|
||||||
|
if u := tw.createGetPathAndParams("bob/foo", p); u != "bob/foo?hello=world" {
|
||||||
|
t.Error("bad url", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBase64EncodedBearerTokenCredentials(t *testing.T) {
|
||||||
|
key := "xvz1evFS4wEEPTGEFPHBog"
|
||||||
|
secret := "L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg"
|
||||||
|
expt := "eHZ6MWV2RlM0d0VFUFRHRUZQSEJvZzpMOHFxOVBaeVJnNmllS0dFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw=="
|
||||||
|
tapi := new(TwitterApi)
|
||||||
|
if res := tapi.GetBase64EncodedBearerTokenCredentials(key, secret); res != expt {
|
||||||
|
t.Error("expected: ", expt, "\nbut received:", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
+293
@@ -0,0 +1,293 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
t.Error("bad request verb")
|
||||||
|
}
|
||||||
|
if auth := r.Header.Get("Authorization"); auth != "Bearer access_token" {
|
||||||
|
t.Error("bad authorization in header :", auth)
|
||||||
|
}
|
||||||
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" {
|
||||||
|
t.Error("bad content-type in header ", contentType)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
|
||||||
|
if res, err := tw.Get("/"); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else {
|
||||||
|
ioutil.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPost(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
t.Error("bad request verb")
|
||||||
|
}
|
||||||
|
if auth := r.Header.Get("Authorization"); auth != "Bearer access_token" {
|
||||||
|
t.Error("bad authorization in header :", auth)
|
||||||
|
}
|
||||||
|
if contentType := r.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" {
|
||||||
|
t.Error("bad content-type in header ", contentType)
|
||||||
|
}
|
||||||
|
if body, err := ioutil.ReadAll(r.Body); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if string(body) != `{"data": "is_hugue"}` {
|
||||||
|
t.Error("bad body", string(body))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
|
||||||
|
if res, err := tw.Post("/", `{"data": "is_hugue"}`); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else {
|
||||||
|
ioutil.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAndDeserializeError(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, `{"error": "bar"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
m := make(map[string]string)
|
||||||
|
if err := tw.GetAndDeserialize("/foo", map[string]string{}, &m); err == nil {
|
||||||
|
t.Error("shoud have an error message here")
|
||||||
|
} else if err.Error() != `{"error": "bar"}` {
|
||||||
|
t.Error("error message should propagate here")
|
||||||
|
} else if terr, ok := err.(*TwitterErr); !ok {
|
||||||
|
t.Error("the error should be a twitter error")
|
||||||
|
} else if terr.Status != 404 {
|
||||||
|
t.Error("bad status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAndDeserialize(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Query().Get("foo") != "bar" {
|
||||||
|
t.Error("bad params")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{"foo": "bar"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
m := make(map[string]string)
|
||||||
|
if err := tw.GetAndDeserialize("/foo", map[string]string{"foo": "bar"}, &m); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if m["foo"] != "bar" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostAndDeserialize(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if body, err := ioutil.ReadAll(r.Body); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if string(body) != `val=7` {
|
||||||
|
t.Error("bad body")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{"foo": "bar"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
m := make(map[string]string)
|
||||||
|
|
||||||
|
if err := tw.PostAndDeserialize("/foo", map[string]string{"val": "7"}, &m); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if m["foo"] != "bar" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTwitterIdByScreenName(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if url_ := r.URL.String(); url_ != "/users/lookup.json?include_entities=id&screen_name=bobLeChef" {
|
||||||
|
t.Error("bad url :", url_)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `[{"id": 1492}]`)
|
||||||
|
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
|
||||||
|
if id, err := tw.GetTwitterIdByScreenName("bobLeChef"); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if id != 1492 {
|
||||||
|
t.Error("found bad id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFollowerByCursor(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if url_ := r.URL.String(); url_ != "/followers/list.json?count=200&cursor=89&screen_name=bobLeChef&skip_status=true" {
|
||||||
|
t.Error("bad url :", url_)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{"users": [{"id": 1492, "screen_name": "bob_le_chef"}], "next_cursor_str": "1793"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
|
||||||
|
followersListC := tw.GetFollowerByCursor("bobLeChef", "89")
|
||||||
|
followers := <-followersListC
|
||||||
|
|
||||||
|
if followers.NextCursor != "1793" {
|
||||||
|
t.Error("bad cursor")
|
||||||
|
} else if len(followers.Followers) != 1 {
|
||||||
|
t.Error("bad number of followers")
|
||||||
|
} else if user := followers.Followers[0]; !reflect.DeepEqual(user, &User{"bob_le_chef", 1492}) {
|
||||||
|
t.Error("bad user", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFollowerIdsByCursor(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if url_ := r.URL.String(); url_ != "/followers/ids.json?count=5000&cursor=89&screen_name=bobLeChef" {
|
||||||
|
t.Error("bad url :", url_)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{"ids": [1492], "next_cursor_str": "1793"}`)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
tw := NewTwitterApi(ts.URL, "access_token")
|
||||||
|
|
||||||
|
followersListC := tw.GetGetFollowerIdsByCursor("bobLeChef", "89")
|
||||||
|
|
||||||
|
followers := <-followersListC
|
||||||
|
|
||||||
|
if followers.NextCursor != "1793" {
|
||||||
|
t.Error("bad cursor")
|
||||||
|
} else if len(followers.Followers) != 1 {
|
||||||
|
t.Error("bad number of followers", len(followers.Followers))
|
||||||
|
} else if id := followers.Followers[0]; id != 1492 {
|
||||||
|
t.Error("bad id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockFollowerGetter struct {
|
||||||
|
T *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFollowerGetter) GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList {
|
||||||
|
followerListC := make(chan *FollowerList)
|
||||||
|
go func() {
|
||||||
|
u1 := &User{"nat", 78789}
|
||||||
|
u2 := &User{"jude", 78789}
|
||||||
|
u3 := &User{"alice", 78789}
|
||||||
|
u4 := &User{"bob", 78789}
|
||||||
|
switch cursor {
|
||||||
|
case "-1":
|
||||||
|
followerListC <- &FollowerList{"1", []*User{u1, u2}}
|
||||||
|
case "1":
|
||||||
|
followerListC <- &FollowerList{"0", []*User{u3, u4}}
|
||||||
|
case "0":
|
||||||
|
default:
|
||||||
|
m.T.Error("bad cursor", cursor)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return followerListC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFollowerGetter) GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList {
|
||||||
|
followerListC := make(chan *FollowerIDList)
|
||||||
|
go func() {
|
||||||
|
switch cursor {
|
||||||
|
case "-1":
|
||||||
|
followerListC <- &FollowerIDList{"1", []uint64{1, 2}}
|
||||||
|
case "1":
|
||||||
|
followerListC <- &FollowerIDList{"0", []uint64{3, 4}}
|
||||||
|
case "0":
|
||||||
|
default:
|
||||||
|
m.T.Error("bad cursor", cursor)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return followerListC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockFollowerGetter) GetScreenNameOfUsersByIds(ids []uint64) <-chan string {
|
||||||
|
ret := make(chan string)
|
||||||
|
go func() {
|
||||||
|
for _, id := range ids {
|
||||||
|
ret <- fmt.Sprintf("%v", id)
|
||||||
|
}
|
||||||
|
close(ret)
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAllStringFromChannel(c <-chan string) []string {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
for s := range c {
|
||||||
|
ret = append(ret, s)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAllUInt64FromChannel(c <-chan uint64) []uint64 {
|
||||||
|
ret := make([]uint64, 0)
|
||||||
|
for s := range c {
|
||||||
|
ret = append(ret, s)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFollowerScreenNames(t *testing.T) {
|
||||||
|
fg := &MockFollowerGetter{t}
|
||||||
|
followerC := GetFollowerScreenNames(fg, "justinBieber")
|
||||||
|
followers := readAllStringFromChannel(followerC)
|
||||||
|
expected := []string{"nat", "jude", "alice", "bob"}
|
||||||
|
if !reflect.DeepEqual(followers, expected) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFollowerIds(t *testing.T) {
|
||||||
|
fg := &MockFollowerGetter{t}
|
||||||
|
ids := readAllUInt64FromChannel(GetFollowerIds(fg, "justinBieber"))
|
||||||
|
expected := []uint64{1, 2, 3, 4}
|
||||||
|
if !reflect.DeepEqual(ids, expected) {
|
||||||
|
t.Error(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsCommaSeparatedString(t *testing.T) {
|
||||||
|
tw := new(TwitterApi)
|
||||||
|
if s := tw.asCommaSeparatedString([]uint64{10765432100123456789, 78}); s != "10765432100123456789,78" {
|
||||||
|
t.Error(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64Range(n int) []uint64 {
|
||||||
|
ret := make([]uint64, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
ret[i] = uint64(i)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTwitterScreenNameByIds(t *testing.T) {
|
||||||
|
fg := &MockFollowerGetter{t}
|
||||||
|
ids := uint64Range(233)
|
||||||
|
ret := make(map[string]bool)
|
||||||
|
for name := range GetTwitterScreenNameByIds(fg, makeUInt64Channel(ids...)) {
|
||||||
|
ret[name] = true
|
||||||
|
}
|
||||||
|
if len(ret) != 233 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user