├── .gitignore ├── instagram ├── likes.go ├── comments.go ├── config_test.go.template ├── helpers.go ├── locations.go ├── tags.go ├── media.go ├── iterate_test.go ├── pagination.go ├── relationships.go ├── response.go ├── iterate.go ├── users.go ├── example_test.go ├── types.go ├── core.go └── instagram_test.go ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | instagram/config_test.go 25 | -------------------------------------------------------------------------------- /instagram/likes.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get a list of users who have liked this media. 9 | // Required Scope: likes 10 | // Gets /media/{media-id}/likes 11 | func (api *Api) GetMediaLikes(mediaId string, params url.Values) (res *UsersResponse, err error) { 12 | res = new(UsersResponse) 13 | err = api.get(fmt.Sprintf("/media/%s/likes", mediaId), params, res) 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /instagram/comments.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get a full list of comments on a media. 9 | // Required Scope: comments 10 | // Gets /media/{media-id}/comments 11 | func (api *Api) GetMediaComments(mediaId string, params url.Values) (res *CommentsResponse, err error) { 12 | res = new(CommentsResponse) 13 | err = api.get(fmt.Sprintf("/media/%s/comments", mediaId), params, res) 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /instagram/config_test.go.template: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | // Copy this to config_test.go (its ignored by git) 4 | // And fill in your test clientID, clientSecret, authorization token and secret 5 | // You can get the client information from http://instagram.com/developer/clients/manage/ 6 | // And you'll have to authorize with that client to get a token 7 | // You don't NEED access tokens per se but some requests require it. 8 | 9 | var TestConfig map[string]string = map[string]string{ 10 | "client_id": "", 11 | "access_token": "", 12 | "my_id": "", // The authenticated user's ID 13 | } -------------------------------------------------------------------------------- /instagram/helpers.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | type StringUnixTime string 10 | 11 | func (s StringUnixTime) Time() (t time.Time, err error) { 12 | unix, err := strconv.ParseInt(string(s), 10, 64) 13 | if err != nil { 14 | return 15 | } 16 | 17 | t = time.Unix(unix, 0) 18 | return 19 | } 20 | 21 | // Sometimes location Id is a string and sometimes its an integer 22 | type LocationId interface{} 23 | 24 | func ParseLocationId(lid LocationId) string { 25 | if lid == nil { 26 | return "" 27 | } 28 | if slid, ok := lid.(string); ok { 29 | return slid 30 | } 31 | if ilid, ok := lid.(int64); ok { 32 | return fmt.Sprintf("%d", ilid) 33 | } 34 | return "" 35 | } 36 | -------------------------------------------------------------------------------- /instagram/locations.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get information about a location. 9 | // Gets /locations/{location-id} 10 | func (api *Api) GetLocation(locationId string, params url.Values) (res *LocationResponse, err error) { 11 | res = new(LocationResponse) 12 | err = api.get(fmt.Sprintf("/locations/%s", locationId), params, res) 13 | return 14 | } 15 | 16 | // Get a list of recent media objects from a given location. May return a mix of both image and video types. 17 | // Gets /locations/{location-id}/media/recent 18 | func (api *Api) GetLocationRecentMedia(locationId string, params url.Values) (res *PaginatedMediasResponse, err error) { 19 | res = new(PaginatedMediasResponse) 20 | err = api.get(fmt.Sprintf("/locations/%s/media/recent", locationId), params, res) 21 | return 22 | } 23 | 24 | // Search for a location by geographic coordinate. 25 | // Gets /locations/search 26 | func (api *Api) GetLocationSearch(params url.Values) (res *LocationsResponse, err error) { 27 | res = new(LocationsResponse) 28 | err = api.get("/locations/search", params, res) 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jon Eisen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /instagram/tags.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get information about a tag object. 9 | // Gets /tags/{tag-name} 10 | func (api *Api) GetTag(tagName string, params url.Values) (res *TagResponse, err error) { 11 | res = new(TagResponse) 12 | err = api.get(fmt.Sprintf("/tags/%s", tagName), params, res) 13 | return 14 | } 15 | 16 | // Get a list of recently tagged media. Note that this media is ordered by when the media was tagged with this tag, rather than the order it was posted. Use the max_tag_id and min_tag_id parameters in the pagination response to paginate through these objects. Can return a mix of image and video types. 17 | // Gets /tags/{tag-name}/media/recent 18 | func (api *Api) GetTagRecentMedia(tagName string, params url.Values) (res *PaginatedMediasResponse, err error) { 19 | res = new(PaginatedMediasResponse) 20 | err = api.get(fmt.Sprintf("/tags/%s/media/recent", tagName), params, res) 21 | return 22 | } 23 | 24 | // Search for tags by name. Results are ordered first as an exact match, then by popularity. Short tags will be treated as exact matches. 25 | // Gets /tags/search 26 | func (api *Api) GetTagSearch(params url.Values) (res *TagsResponse, err error) { 27 | res = new(TagsResponse) 28 | err = api.get("/tags/search", params, res) 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /instagram/media.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get information about a media object. The returned type key will allow you to differentiate between image and video media. 9 | // Note: if you authenticate with an OAuth Token, you will receive the user_has_liked key which quickly tells you whether the current user has liked this media item. 10 | // Gets /media/{media-id} 11 | func (api *Api) GetMedia(mediaId string, params url.Values) (res *MediaResponse, err error) { 12 | res = new(MediaResponse) 13 | err = api.get(fmt.Sprintf("/media/%s", mediaId), params, res) 14 | return 15 | } 16 | 17 | // Search for media in a given area. The default time span is set to 5 days. The time span must not exceed 7 days. Defaults time stamps cover the last 5 days. Can return mix of image and video types. 18 | // Gets /media/search 19 | func (api *Api) GetMediaSearch(params url.Values) (res *MediasResponse, err error) { 20 | res = new(MediasResponse) 21 | err = api.get("/media/search", params, res) 22 | return 23 | } 24 | 25 | // No available endpoints in the new IG Api 26 | // reference: https://www.instagram.com/developer/endpoints/media/ 27 | // Get a list of what media is most popular at the moment. Can return mix of image and video types. 28 | // Gets /media/popular 29 | // func (api *Api) GetMediaPopular(params url.Values) (res *MediasResponse, err error) { 30 | // res = new(MediasResponse) 31 | // err = api.get("/media/popular", params, res) 32 | // return 33 | // } 34 | -------------------------------------------------------------------------------- /instagram/iterate_test.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestIterate_GetUserFollowedBy(t *testing.T) { 9 | res, err := api.GetUserFollowedBy(values("count", "5")) 10 | checkRes(t, res.Meta, err) 11 | 12 | doneChan := make(chan bool) // This is only needed if you want to close early 13 | defer close(doneChan) 14 | 15 | userChan, errChan := api.IterateUsers(res, doneChan) 16 | 17 | i := 0 18 | for user := range userChan { 19 | if user.Id == "" { 20 | t.Error("user has empty id", user) 21 | } 22 | i++ 23 | if i > 19 { 24 | // breaking early 25 | doneChan <- true 26 | // userChan should close immediately afterward, exiting the loop, and not closing the channel again 27 | } 28 | } 29 | 30 | // should be closed 31 | if u, ok := <-userChan; ok { 32 | t.Error("User Channel shouldn't have any more data on it. It should close!", u) 33 | } 34 | 35 | if err := <-errChan; err != nil { 36 | t.Error(err) 37 | } 38 | } 39 | 40 | func TestIterate_GetUserRecentMedia(t *testing.T) { 41 | params := url.Values{} 42 | params.Set("count", "2") // 5 images in this set. Get them 2 at time 43 | params.Set("max_timestamp", "1384161094") 44 | params.Set("min_timestamp", "1382656250") 45 | res, err := api.GetUserRecentMedia(ladygaga_id, params) 46 | checkRes(t, res.Meta, err) 47 | 48 | mediaChan, errChan := api.IterateMedia(res, nil) 49 | for media := range mediaChan { 50 | if media.User.Username != "ladygaga" { 51 | t.Error("Got a media with wrong username?", media.User) 52 | } 53 | } 54 | if err := <-errChan; err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /instagram/pagination.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | // Get the next page of media 9 | func (api *Api) NextMedias(mp *MediaPagination) (res *PaginatedMediasResponse, err error) { 10 | res = new(PaginatedMediasResponse) 11 | err = api.next(mp.Pagination, res) 12 | return 13 | } 14 | 15 | // Get the next page of user 16 | func (api *Api) NextUsers(up *UserPagination) (res *PaginatedUsersResponse, err error) { 17 | res = new(PaginatedUsersResponse) 18 | err = api.next(up.Pagination, res) 19 | return 20 | } 21 | 22 | func (api *Api) next(p *Pagination, res interface{}) error { 23 | done, uri, path, uriParams, err := p.NextPage() 24 | if err != nil || done == true { 25 | return err 26 | } 27 | 28 | // Sign params if using the secure api 29 | if api.EnforceSignedRequest { 30 | uriParams = signParams(path, uriParams, api.ClientSecret) 31 | } 32 | 33 | req, err := buildGetRequest(uri, uriParams) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return api.do(req, res) 39 | } 40 | 41 | // Return the next page uri and parameters 42 | func (p *Pagination) NextPage() (done bool, uri string, path string, params url.Values, err error) { 43 | if p == nil || p.NextUrl == "" { 44 | // We're done. Theres no more pages 45 | done = true 46 | return 47 | } 48 | 49 | urlStruct, err := url.Parse(p.NextUrl) 50 | if err != nil { 51 | return 52 | } 53 | 54 | params = urlStruct.Query() 55 | // Remove `sig` key that was set by the initial request 56 | params.Del("sig") 57 | urlStruct.RawQuery = "" 58 | 59 | done = false 60 | path = strings.Replace(urlStruct.Path, "/v1", "", 1) 61 | uri = urlStruct.String() 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /instagram/relationships.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // for relationships, has been modified the scope in IG new Endpoint API 9 | // reference: https://www.instagram.com/developer/endpoints/relationships/ 10 | 11 | // Get the list of users this user follows. 12 | // Required Scope: relationships 13 | // Gets /users/self/follows 14 | func (api *Api) GetUserFollows(params url.Values) (res *PaginatedUsersResponse, err error) { 15 | res = new(PaginatedUsersResponse) 16 | err = api.get(fmt.Sprintf("/users/self/follows"), params, res) 17 | return 18 | } 19 | 20 | // Get the list of users this user follows. 21 | // Required Scope: relationships 22 | // Gets /users/self/followed-by 23 | func (api *Api) GetUserFollowedBy(params url.Values) (res *PaginatedUsersResponse, err error) { 24 | res = new(PaginatedUsersResponse) 25 | err = api.get(fmt.Sprintf("/users/self/followed-by"), params, res) 26 | return 27 | } 28 | 29 | // List the users who have requested this user's permission to follow. 30 | // Required Scope: relationships 31 | // Gets /users/self/requested-by 32 | func (api *Api) GetUserRequestedBy(params url.Values) (res *UsersResponse, err error) { 33 | res = new(UsersResponse) 34 | err = api.get("/users/self/requested-by", params, res) 35 | return 36 | } 37 | 38 | // Get information about a relationship to another user. 39 | // Required Scope: relationships 40 | // Gets /users/{user-id}/relationship 41 | func (api *Api) GetUserRelationship(userId string, params url.Values) (res *RelationshipResponse, err error) { 42 | res = new(RelationshipResponse) 43 | err = api.get(fmt.Sprintf("/users/%s/relationship", userId), params, res) 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /instagram/response.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | type UserResponse struct { 4 | MetaResponse 5 | User *User `json:"data"` 6 | } 7 | 8 | type UsersResponse struct { 9 | MetaResponse 10 | Users []User `json:"data"` 11 | } 12 | 13 | type PaginatedUsersResponse struct { 14 | UsersResponse 15 | Pagination *UserPagination 16 | } 17 | 18 | type MediaResponse struct { 19 | MetaResponse 20 | Media *Media `json:"data"` 21 | } 22 | 23 | type MediasResponse struct { 24 | MetaResponse 25 | Medias []Media `json:"data"` 26 | } 27 | 28 | type PaginatedMediasResponse struct { 29 | MediasResponse 30 | Pagination *MediaPagination 31 | } 32 | 33 | type CommentsResponse struct { 34 | MetaResponse 35 | Comments []Comment `json:"data"` 36 | } 37 | 38 | type TagResponse struct { 39 | MetaResponse 40 | Tag *Tag `json:"data"` 41 | } 42 | 43 | type TagsResponse struct { 44 | MetaResponse 45 | Tags []Tag `json:"data"` 46 | } 47 | 48 | type LocationResponse struct { 49 | MetaResponse 50 | Location *Location `json:"data"` 51 | } 52 | 53 | type LocationsResponse struct { 54 | MetaResponse 55 | Locations []Location `json:"data"` 56 | } 57 | 58 | type RelationshipResponse struct { 59 | MetaResponse 60 | Relationship *Relationship `json:"data"` 61 | } 62 | 63 | type MetaResponse struct { 64 | Meta *Meta 65 | } 66 | 67 | type Pagination struct { 68 | NextUrl string `json:"next_url"` 69 | NextMaxId string `json:"next_max_id"` 70 | 71 | // Used only on GetTagRecentMedia() 72 | NextMaxTagId string `json:"next_max_tag_id"` 73 | // Used only on GetTagRecentMedia() 74 | MinTagId string `json:"min_tag_id"` 75 | } 76 | 77 | type Meta struct { 78 | Code int 79 | ErrorType string `json:"error_type"` 80 | ErrorMessage string `json:"error_message"` 81 | } 82 | 83 | // MediaPagination will give you an easy way to request the next page of media. 84 | type MediaPagination struct { 85 | *Pagination 86 | } 87 | 88 | // UserPagination will give you an easy way to request the next page of media. 89 | type UserPagination struct { 90 | *Pagination 91 | } 92 | -------------------------------------------------------------------------------- /instagram/iterate.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | // IterateMedia makes pagination easy by converting the repeated api.NextMedias() call to a channel of media. 4 | // Media will be passed in the reverse order of individual requests, for instance GetUserRecentMedia will go in reverse CreatedTime order. 5 | // If you desire to break early, pass in a doneChan and close when you are breaking from iteration 6 | func (api *Api) IterateMedia(res *PaginatedMediasResponse, doneChan <-chan bool) (<-chan *Media, <-chan error) { 7 | mediaChan := make(chan *Media) 8 | errChan := make(chan error, 1) 9 | 10 | go func() { 11 | defer close(mediaChan) 12 | defer close(errChan) 13 | 14 | for { 15 | if res == nil { 16 | return 17 | } 18 | 19 | if len(res.Medias) == 0 { 20 | // No more Medias 21 | return 22 | } 23 | 24 | // Iterate backwards 25 | for i := len(res.Medias) - 1; i >= 0; i-- { 26 | select { 27 | case mediaChan <- &res.Medias[i]: 28 | case <-doneChan: 29 | return 30 | } 31 | } 32 | 33 | // Paginate to next response 34 | var err error 35 | res, err = api.NextMedias(res.Pagination) 36 | if err != nil { 37 | errChan <- err 38 | return 39 | } 40 | } 41 | }() 42 | 43 | return mediaChan, errChan 44 | } 45 | 46 | // IterateUsers makes pagination easy by converting the repeated api.NextUsers() call to a channel of users. 47 | // Users will be passed in the reverse order of individual requests. 48 | // If you desire to break early, pass in a doneChan and close when you are breaking from iteration 49 | func (api *Api) IterateUsers(res *PaginatedUsersResponse, doneChan <-chan bool) (<-chan *User, <-chan error) { 50 | userChan := make(chan *User) 51 | errChan := make(chan error) 52 | 53 | go func() { 54 | defer close(userChan) 55 | defer close(errChan) 56 | 57 | for { 58 | if res == nil { 59 | return 60 | } 61 | 62 | if len(res.Users) == 0 { 63 | // No more users 64 | return 65 | } 66 | 67 | // Iterate backwards 68 | for i := len(res.Users) - 1; i >= 0; i-- { 69 | select { 70 | case userChan <- &res.Users[i]: 71 | case <-doneChan: 72 | return 73 | } 74 | } 75 | 76 | // Paginate to next response 77 | var err error 78 | res, err = api.NextUsers(res.Pagination) 79 | if err != nil { 80 | errChan <- err 81 | return 82 | } 83 | } 84 | }() 85 | 86 | return userChan, errChan 87 | } 88 | -------------------------------------------------------------------------------- /instagram/users.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Get basic information about a user. 9 | // Gets /users/{user-id} 10 | func (api *Api) GetUser(userId string, params url.Values) (res *UserResponse, err error) { 11 | res = new(UserResponse) 12 | err = api.get(fmt.Sprintf("/users/%s", userId), params, res) 13 | return 14 | } 15 | 16 | // Get basic information about authenticated user. 17 | // Gets /users/self 18 | func (api *Api) GetSelf() (res *UserResponse, err error) { 19 | return api.GetUser("self", nil) 20 | } 21 | 22 | // Endpoint is not available in the new IG Api 23 | // reference: https://www.instagram.com/developer/endpoints 24 | // See the authenticated user's feed. May return a mix of both image and video types. 25 | // Gets /users/self/feed 26 | // func (api *Api) GetUserFeed(params url.Values) (res *PaginatedMediasResponse, err error) { 27 | // res = new(PaginatedMediasResponse) 28 | // err = api.get("/users/self/feed", params, res) 29 | // return 30 | // } 31 | 32 | // Get the most recent media published by a user. May return a mix of both image and video types. 33 | // Gets /users/{user-id}/media/recent 34 | func (api *Api) GetUserRecentMedia(userId string, params url.Values) (res *PaginatedMediasResponse, err error) { 35 | res = new(PaginatedMediasResponse) 36 | err = api.get(fmt.Sprintf("/users/%s/media/recent", userId), params, res) 37 | return 38 | } 39 | 40 | // See the authenticated user's list of media they've liked. May return a mix of both image and video types. 41 | // Note: This list is ordered by the order in which the user liked the media. Private media is returned as long as the authenticated user has permission to view that media. Liked media lists are only available for the currently authenticated user. 42 | // Gets /users/self/media/liked 43 | func (api *Api) GetUserLikedMedia(params url.Values) (res *PaginatedMediasResponse, err error) { 44 | res = new(PaginatedMediasResponse) 45 | err = api.get("/users/self/media/liked", params, res) 46 | return 47 | } 48 | 49 | // Search for a user by name. 50 | // Gets /users/search 51 | func (api *Api) GetUserSearch(params url.Values) (res *UsersResponse, err error) { 52 | res = new(UsersResponse) 53 | err = api.get("/users/search", params, res) 54 | return 55 | } 56 | 57 | // Verify a valid client keys and user tokens by making a small request 58 | func (api *Api) VerifyCredentials() (ok bool, err error) { 59 | _, err = api.GetSelf() 60 | return err == nil, err 61 | } 62 | -------------------------------------------------------------------------------- /instagram/example_test.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // ExampleNew sets up the whole instagram API 9 | func ExampleNew() { 10 | apiAuthenticatedUser := New("client_key", "secret", "", true) 11 | if ok, err := apiAuthenticatedUser.VerifyCredentials(); !ok { 12 | panic(err) 13 | } 14 | fmt.Println("Successfully created instagram.Api with user credentials") 15 | } 16 | 17 | // ExampleApi_GetUser shows how to get a user object 18 | func ExampleApi_GetUser() { 19 | // *** or *** 20 | api := New("client_id", "client_secret", "access_token", true) 21 | 22 | userResponse, err := api.GetUser("user-id", nil) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | user := userResponse.User 28 | processUser(user) 29 | } 30 | 31 | // ExampleApi_GetUserSearch_Params shows how to use parameters 32 | func ExampleApi_GetUserSearch_Params() { 33 | // *** need *** 34 | api := New("", "client_secret", "access_token", true) 35 | 36 | params := url.Values{} 37 | params.Set("count", "5") // Get 5 users 38 | params.Set("q", "jack") // Search for user "jack" 39 | 40 | usersResponse, err := api.GetUserSearch(params) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | for _, user := range usersResponse.Users { 46 | processUser(&user) 47 | } 48 | } 49 | 50 | // ExampleApi_GetUserRecentMedia : Get the most recent media published by the owner 51 | func ExampleApi_GetUserRecentMedia() { 52 | // *** or *** 53 | api := New("client_id", "client_secret", "access_token", true) 54 | 55 | params := url.Values{} 56 | params.Set("count", "3") // 4 images in this set 57 | params.Set("max_timestamp", "1466809870") 58 | params.Set("min_timestamp", "1396751898") 59 | mediasResponse, err := api.GetUserRecentMedia(ccistulli_id, params) 60 | 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | for _, media := range mediasResponse.Medias { 66 | processMedia(&media) 67 | } 68 | } 69 | 70 | // ExampleApi_IterateMedia shows how to use iteration on a channel to avoid the complex pagination calls 71 | func ExampleApi_IterateMedia() { 72 | // *** or *** 73 | api := New("client_id", "client_secret", "access_token", true) 74 | 75 | mediasResponse, err := api.GetUserRecentMedia("user-id", nil) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | // Stop 30 days ago 81 | doneChan := make(chan bool) 82 | 83 | mediaIter, errChan := api.IterateMedia(mediasResponse, doneChan /* optional */) 84 | for media := range mediaIter { 85 | processMedia(media) 86 | 87 | if isDone(media) { 88 | close(doneChan) // Signal to iterator to quit 89 | break 90 | } 91 | } 92 | 93 | // When mediaIter is closed, errChan will either have a single error on it or it will have been closed so this is safe. 94 | if err := <-errChan; err != nil { 95 | panic(err) 96 | } 97 | } 98 | 99 | func processMedia(m *Media) {} 100 | func isDone(m *Media) bool { 101 | return false 102 | } 103 | func processUser(u *User) {} 104 | -------------------------------------------------------------------------------- /instagram/types.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | // Instagram User Object. Note that user objects are not always fully returned. 4 | // Be sure to see the descriptions on the instagram documentation for any given endpoint. 5 | type User struct { 6 | Id string `json:"id"` 7 | Username string `json:"username"` 8 | FirstName string `json:"first_name"` 9 | LastName string `json:"last_name"` 10 | FullName string `json:"full_name"` 11 | ProfilePicture string `json:"profile_picture"` 12 | Bio string 13 | Website string 14 | Counts *UserCounts 15 | } 16 | 17 | // Instagram User Counts object. Returned on User objects 18 | type UserCounts struct { 19 | Media int64 20 | Follows int64 21 | FollowedBy int64 `json:"followed_by"` 22 | } 23 | 24 | // Instagram Media object 25 | type Media struct { 26 | Type string 27 | Id string 28 | UsersInPhoto []UserPosition `json:"users_in_photo"` 29 | Filter string 30 | Tags []string 31 | Comments *Comments 32 | Caption *Caption 33 | Likes *Likes 34 | Link string 35 | User *User 36 | CreatedTime StringUnixTime `json:"created_time"` 37 | Images *Images 38 | Videos *Videos 39 | Location *Location 40 | UserHasLiked bool `json:"user_has_liked"` 41 | Attribution *Attribution 42 | } 43 | 44 | // A pair of user object and position 45 | type UserPosition struct { 46 | User *User 47 | Position *Position 48 | } 49 | 50 | // A position in a media 51 | type Position struct { 52 | X float64 53 | Y float64 54 | } 55 | 56 | // Instagram tag 57 | type Tag struct { 58 | MediaCount int64 `json:"media_count"` 59 | Name string 60 | } 61 | 62 | type Comments struct { 63 | Count int64 64 | Data []Comment 65 | } 66 | 67 | type Comment struct { 68 | CreatedTime StringUnixTime `json:"created_time"` 69 | Text string 70 | From *User 71 | Id string 72 | } 73 | 74 | type Caption Comment 75 | 76 | type Likes struct { 77 | Count int64 78 | Data []User 79 | } 80 | 81 | type Images struct { 82 | LowResolution *Image `json:"low_resolution"` 83 | Thumbnail *Image 84 | StandardResolution *Image `json:"standard_resolution"` 85 | } 86 | 87 | type Image struct { 88 | Url string 89 | Width int64 90 | Height int64 91 | } 92 | 93 | type Videos struct { 94 | LowResolution *Video `json:"low_resolution"` 95 | StandardResolution *Video `json:"standard_resolution"` 96 | } 97 | 98 | type Video Image 99 | 100 | type Location struct { 101 | Id LocationId 102 | Name string 103 | Latitude float64 104 | Longitude float64 105 | } 106 | 107 | type Relationship struct { 108 | IncomingStatus string `json:"incoming_status"` 109 | OutgoingStatus string `json:"outgoing_status"` 110 | } 111 | 112 | // If another app uploaded the media, then this is the place it is given. As of 11/2013, Hipstamic is the only allowed app 113 | type Attribution struct { 114 | Website string 115 | ItunesUrl string 116 | Name string 117 | } 118 | -------------------------------------------------------------------------------- /instagram/core.go: -------------------------------------------------------------------------------- 1 | // Package instagram provides a minimialist instagram API wrapper. 2 | package instagram 3 | 4 | import ( 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | "sort" 15 | ) 16 | 17 | var ( 18 | baseUrl = "https://api.instagram.com/v1" 19 | ) 20 | 21 | type Api struct { 22 | ClientId string 23 | ClientSecret string 24 | AccessToken string 25 | EnforceSignedRequest bool 26 | Header http.Header 27 | } 28 | 29 | // Create an API with either a ClientId OR an accessToken. Only one is required. Access tokens are preferred because they keep rate limiting down. 30 | // If enforceSignedRequest is set to true, then clientSecret is required 31 | func New(clientId string, clientSecret string, accessToken string, enforceSignedRequest bool) *Api { 32 | if clientId == "" && accessToken == "" { 33 | panic("ClientId or AccessToken must be given to create an Api") 34 | } 35 | 36 | if enforceSignedRequest && clientSecret == "" { 37 | panic("ClientSecret is required for signed request") 38 | } 39 | 40 | return &Api{ 41 | ClientId: clientId, 42 | ClientSecret: clientSecret, 43 | AccessToken: accessToken, 44 | EnforceSignedRequest: enforceSignedRequest, 45 | } 46 | } 47 | 48 | // -- Implementation of request -- 49 | func signParams(path string, params url.Values, clientSecret string) url.Values { 50 | message := path 51 | keys := []string{} 52 | 53 | for k := range params { 54 | keys = append(keys, k) 55 | } 56 | 57 | sort.Strings(keys) 58 | 59 | for _, v := range keys { 60 | message += "|" + v + "=" + params.Get(v) 61 | } 62 | 63 | hash := hmac.New(sha256.New, []byte(clientSecret)) 64 | hash.Write([]byte(message)) 65 | 66 | params.Set("sig", hex.EncodeToString(hash.Sum(nil))) 67 | return params 68 | } 69 | 70 | func buildGetRequest(urlStr string, params url.Values) (*http.Request, error) { 71 | u, err := url.Parse(urlStr) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | // If we are getting, then we can't merge query params 77 | if params != nil { 78 | if u.RawQuery != "" { 79 | return nil, fmt.Errorf("Cannot merge query params in urlStr and params") 80 | } 81 | u.RawQuery = params.Encode() 82 | } 83 | 84 | return http.NewRequest("GET", u.String(), nil) 85 | } 86 | 87 | func (api *Api) extendParams(p url.Values) url.Values { 88 | if p == nil { 89 | p = url.Values{} 90 | } 91 | if api.AccessToken != "" { 92 | p.Set("access_token", api.AccessToken) 93 | } else { 94 | p.Set("client_id", api.ClientId) 95 | } 96 | return p 97 | } 98 | 99 | func (api *Api) get(path string, params url.Values, r interface{}) error { 100 | params = api.extendParams(params) 101 | // Sign request if ForceSignedRequest is set to true 102 | if api.EnforceSignedRequest { 103 | params = signParams(path, params, api.ClientSecret) 104 | } 105 | 106 | req, err := buildGetRequest(urlify(path), params) 107 | if err != nil { 108 | return err 109 | } 110 | return api.do(req, r) 111 | } 112 | 113 | func (api *Api) do(req *http.Request, r interface{}) error { 114 | resp, err := http.DefaultClient.Do(req) 115 | if err != nil { 116 | return err 117 | } 118 | defer func() { 119 | io.CopyN(ioutil.Discard, resp.Body, 512) 120 | resp.Body.Close() 121 | }() 122 | 123 | api.Header = resp.Header 124 | 125 | if resp.StatusCode != 200 { 126 | return apiError(resp) 127 | } 128 | 129 | return decodeResponse(resp.Body, r) 130 | } 131 | 132 | func decodeResponse(body io.Reader, to interface{}) error { 133 | // b, _ := ioutil.ReadAll(body) 134 | // fmt.Println("Body:",string(b)) 135 | // err := json.Unmarshal(b, to) 136 | err := json.NewDecoder(body).Decode(to) 137 | 138 | if err != nil { 139 | return fmt.Errorf("instagram: error decoding body; %s", err.Error()) 140 | } 141 | return nil 142 | } 143 | 144 | func apiError(resp *http.Response) error { 145 | m := new(MetaResponse) 146 | if err := decodeResponse(resp.Body, m); err != nil { 147 | return err 148 | } 149 | 150 | var err MetaError 151 | if m.Meta != nil { 152 | err = MetaError(*m.Meta) 153 | } else { 154 | err = MetaError(Meta{Code: resp.StatusCode, ErrorMessage: resp.Status}) 155 | } 156 | return &err 157 | } 158 | 159 | func urlify(path string) string { 160 | return baseUrl + path 161 | } 162 | 163 | type MetaError Meta 164 | 165 | func (m *MetaError) Error() string { 166 | return fmt.Sprintf("Error making api call: Code %d %s %s", m.Code, m.ErrorType, m.ErrorMessage) 167 | } 168 | 169 | func ensureParams(v url.Values) url.Values { 170 | if v == nil { 171 | return url.Values{} 172 | } 173 | return v 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-instagram 2 | 3 | A [instagram](http://instagram.com) [API](http://instagram.com/developer) wrapper. 4 | 5 | ## Features 6 | 7 | Implemented: 8 | 9 | - All GET requests are implemented 10 | - Both authenticated and unauthenticated requests can be made 11 | - A nice iterator facility for paginated requests 12 | - No `interface{}` data types! (1 exception, see Location.Id note below) 13 | 14 | Todo: 15 | 16 | - Authentication 17 | - POST / DELETE requests (you need special permissions for these so no way to test.) 18 | 19 | ## Documentation 20 | 21 | [Documentation on godoc.org](http://godoc.org/github.com/yanatan16/golang-instagram/instagram) 22 | 23 | ## Install 24 | 25 | ``` 26 | go get github.com/yanatan16/golang-instagram/instagram 27 | ``` 28 | 29 | ## Creation 30 | 31 | ```go 32 | import ( 33 | "github.com/yanatan16/golang-instagram/instagram" 34 | ) 35 | 36 | unauthenticatedApi := &instagram.Api{ 37 | ClientId: "my-client-id", 38 | } 39 | 40 | authenticatedApi := &instagram.Api{ 41 | AccessToken: "my-access-token", 42 | } 43 | 44 | - if enforceSigned false 45 | anotherAuthenticatedApi := instagram.New("", "", "my-access-token", false) 46 | 47 | - if enforceSigned true 48 | anotherAuthenticatedApi := instagram.New("client_id", "client_secret", "my-access-token", false) 49 | 50 | ``` 51 | 52 | ## Usage 53 | 54 | See the [documentation](http://godoc.org/github.com/yanatan16/golang-instagram/instagram), [endpoint examples](https://github.com/yanatan16/golang-instagram/blob/master/instagram/example_test.go), and the [iteration tests](https://github.com/yanatan16/golang-instagram/blob/master/instagram/iterate_test.go) for a deeper dive than whats below. 55 | 56 | ```go 57 | import ( 58 | "fmt" 59 | "github.com/yanatan16/golang-instagram/instagram" 60 | "net/url" 61 | ) 62 | 63 | func DoSomeInstagramApiStuff(accessToken string) { 64 | api := New("", accessToken) 65 | 66 | if ok, err := api.VerifyCredentials(); !ok { 67 | panic(err) 68 | } 69 | 70 | var myId string 71 | 72 | // Get yourself! 73 | if resp, err := api.GetSelf(); err != nil { 74 | panic(err) 75 | } else { 76 | // A response has two fields: Meta which you shouldn't really care about 77 | // And whatever your getting, in this case, a User 78 | me := resp.User 79 | fmt.Printf("My userid is %s and I have %d followers\n", me.Id, me.Counts.FollowedBy) 80 | } 81 | 82 | params := url.Values{} 83 | params.Set("count", "1") 84 | if resp, err := api.GetUserRecentMedia("self" /* this works :) */, params); err != nil { 85 | panic(err) 86 | } else { 87 | if len(resp.Medias) == 0 { // [sic] 88 | panic("I should have some sort of media posted on instagram!") 89 | } 90 | media := resp.Medias[0] 91 | fmt.Println("My last media was a %s with %d comments and %d likes. (url: %s)\n", media.Type, media.Comments.Count, media.Like.Count, media.Link) 92 | } 93 | } 94 | ``` 95 | 96 | There's many more endpoints and a fancy iteration wrapper. Check it out in the code and documentation! 97 | 98 | ## Iteration 99 | 100 | So pagination makes iterating through a list of users or media possible, but its not easy. So, because Go has nice iteration facilities (i.e. `range`), this package includes two useful methods for iterating over paginating: `api.IterateMedias` and `api.IterateUsers`. You can see [the tests](https://github.com/yanatan16/golang-instagram/blob/master/instagram/iterate_test.go) and [the docs](http://godoc.org/github.com/yanatan16/golang-instagram/instagram/#Api.IterateMedia) for more info. 101 | 102 | ```go 103 | // First go and make the original request, passing in any additional parameters you need 104 | res, err := api.GetUserRecentMedia("some-user", params) 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | // If you plan to break early, create a done channel. Pass in nil if you plan to exhaust the pagination 110 | done := make(chan bool) 111 | defer close(done) 112 | 113 | // Here we get back two channels. Don't worry about the error channel for now 114 | medias, errs := api.IterateMedia(res, done) 115 | 116 | for media := range medias { 117 | processMedia(media) 118 | 119 | if doneWithMedia(media) { 120 | // This is how we signal to the iterator to quit early 121 | done <- true 122 | } 123 | } 124 | 125 | // If we exited early due to an error, we can check here 126 | if err, ok := <- errs; ok && err != nil { 127 | panic(err) 128 | } 129 | ``` 130 | 131 | ## Tests 132 | 133 | To run the tests, you'll need at least a `ClientId` (which you can get from [here](http://instagram.com/developer/clients/manage/)), and preferably an authenticated users' `AccessToken`, which you can get from making a request on the [API Console](http://instagram.com/developer/api-console/) 134 | 135 | First, fill in `config_test.go.example` and save it as `config_test.go`. Then run `go test` 136 | 137 | ## Notes 138 | 139 | - Certain methods require an access token so check the official documentation before using an unauthenticated `Api`. Also, there is a 5000 request per hour rate limit on any one ClientId or AccessToken, so it is advisable to use AccessTokens when available. This package will use it if it is given over a ClientId. 140 | - Location.Id is sometimes returned as an integer (in media i think) and sometimes a string. Because of this, we have to call it an `interface{}`. But there is a facility to force it to a string, as follows: 141 | 142 | ```go 143 | var loc Location 144 | stringIdVersion := instagram.ParseLocationId(loc.Id) 145 | ``` 146 | 147 | If anyone can prove to me that they fixed this bug, just let me know and we can change it to a string (all other IDs are strings...) 148 | 149 | - `created_time` fields come back as strings. So theres a handy type `StringUnixTimeStringUnixTime` which has a nice method `func (sut StringUnixTime) Time() (time.Time, error)` that you can use to cast it to a golang time. 150 | - I apologize for using Medias [sic] everywhere, I needed a plural version that isn't spelled the same. 151 | 152 | ## License 153 | 154 | MIT-style. See LICENSE file. 155 | -------------------------------------------------------------------------------- /instagram/instagram_test.go: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var DoAuthorizedRequests bool 13 | var api *Api 14 | var ccistulli_id string = "401243155" 15 | var ladygaga_id string = "184692323" 16 | 17 | func init() { 18 | DoAuthorizedRequests = (TestConfig["access_token"] != "") 19 | if !DoAuthorizedRequests { 20 | fmt.Println("*** Authorized requests will not performed because no access_token was specified in config_test.go") 21 | } 22 | api = createApi() 23 | } 24 | 25 | func TestVerifyCredentials(t *testing.T) { 26 | authorizedRequest(t) 27 | 28 | if ok, err := api.VerifyCredentials(); !ok { 29 | t.Error(err) 30 | } 31 | } 32 | 33 | func TestUser(t *testing.T) { 34 | resp, err := api.GetUser(ccistulli_id, nil) 35 | checkRes(t, resp.Meta, err) 36 | 37 | user := resp.User 38 | if user.Username != "ccistulli" { 39 | t.Error("username isn't right", user.Username) 40 | } 41 | if user.Id != ccistulli_id { 42 | t.Error("id isn't right", user.Id) 43 | } 44 | if user.Counts == nil { 45 | t.Error("user doesn't have counts!") 46 | } else if user.Counts.Media < 28 { 47 | t.Error("Media count is way off", user.Counts) 48 | } 49 | } 50 | 51 | func TestSelf(t *testing.T) { 52 | authorizedRequest(t) 53 | 54 | self, err := api.GetSelf() 55 | checkRes(t, self.Meta, err) 56 | 57 | user, err := api.GetUser(TestConfig["my_id"], nil) 58 | checkRes(t, user.Meta, err) 59 | 60 | if self.User.Id != TestConfig["my_id"] { 61 | t.Error("self user Id isn't my_id") 62 | } 63 | if !reflect.DeepEqual(self, user) { 64 | t.Error("Self != user!?", self, user) 65 | } 66 | } 67 | 68 | func TestGetUserRecentMedia(t *testing.T) { 69 | params := url.Values{} 70 | params.Set("count", "3") // 4 images in this set 71 | params.Set("max_timestamp", "1466809870") 72 | params.Set("min_timestamp", "1396751898") 73 | res, err := api.GetUserRecentMedia(ccistulli_id, params) 74 | checkRes(t, res.Meta, err) 75 | 76 | if len(res.Medias) != 3 { 77 | t.Error("Count didn't apply") 78 | } 79 | 80 | nextRes, err := api.NextMedias(res.Pagination) 81 | checkRes(t, nextRes.Meta, err) 82 | 83 | if len(nextRes.Medias) != 1 { 84 | t.Error("Timestamps didn't apply") 85 | } 86 | 87 | if nextRes.Pagination.Pagination != nil { 88 | t.Error("Pagination should be not valid!", nextRes.Pagination.Pagination) 89 | } 90 | 91 | nextNextRes, err := api.NextMedias(nextRes.Pagination) 92 | if len(nextNextRes.Medias) > 0 { 93 | t.Error("Pagination returned non-nil next request after nil pagination!") 94 | } 95 | } 96 | 97 | func TestGetUserLikedMedia(t *testing.T) { 98 | authorizedRequest(t) 99 | 100 | res, err := api.GetUserLikedMedia(nil) 101 | checkRes(t, res.Meta, err) 102 | 103 | // Can't really do much here. Don't know who you are. 104 | // We can however go through each image and make sure UserHasLiked is true 105 | for _, media := range res.Medias { 106 | if !media.UserHasLiked { 107 | t.Error("Media from GetUserLikedMedia has UserHasLiked=false ?") 108 | } 109 | } 110 | } 111 | 112 | func TestGetUserSearch(t *testing.T) { 113 | term := "jack" 114 | var totalCount = 9 115 | res, err := api.GetUserSearch(values("q", term, "count", strconv.Itoa(totalCount))) // If anyone signs up with the name traf, this could fail 116 | checkRes(t, res.Meta, err) 117 | 118 | // we need to add, maybe error on the IG endpoint 119 | // as specify `COUNT` is the number of users to return 120 | // but its not exactly it return 121 | // if set `COUNT` to 9, it will return 10 users 122 | totalCount++ 123 | if len(res.Users) != totalCount { 124 | t.Error("Users search length not 10? This could mean the search term has an exact match and needs to be changed.") 125 | } 126 | 127 | for _, user := range res.Users { 128 | if user.Id == "" { 129 | t.Error("No ID on a user?") 130 | } else if user.Username == "" { 131 | t.Error("no Username on a user?") 132 | } 133 | } 134 | } 135 | 136 | func TestGetMedia(t *testing.T) { 137 | res, err := api.GetMedia("594914758412103315_2134762", nil) 138 | checkRes(t, res.Meta, err) 139 | 140 | if res.Media.Attribution != nil { 141 | t.Error("Attribution") 142 | } 143 | if res.Media.Videos.LowResolution.Url != "https://scontent.cdninstagram.com/t50.2886-16/11678165_1006895152655426_1085814856_a.mp4" { 144 | t.Error("Videos.LowResolution.Url") 145 | } 146 | if res.Media.Videos.StandardResolution.Width != int64(480) { 147 | t.Error("Videos.StandardResolution.Width") 148 | } 149 | if len(res.Media.Tags) != 0 { 150 | t.Error("Tags") 151 | } 152 | if res.Media.Type != "video" { 153 | t.Error("Type") 154 | } 155 | if res.Media.Location != nil { 156 | t.Error("Location") 157 | } 158 | if res.Media.Comments.Count < 128 { 159 | t.Error("Comments.Count") 160 | } 161 | if res.Media.Filter != "Normal" { 162 | t.Error("Filter") 163 | } 164 | if tm, err := res.Media.CreatedTime.Time(); err != nil || !tm.Equal(time.Unix(1385139387, 0)) { 165 | t.Error("CreatedTime", tm, err) 166 | } 167 | if res.Media.Link != "https://www.instagram.com/p/hBj9Ieym6T/" { 168 | t.Error("Link") 169 | } 170 | if res.Media.Likes.Count < 2000 { 171 | t.Error("Likes.Count") 172 | } 173 | if res.Media.Images.Thumbnail.Height != 150 { 174 | t.Error("Images.Thumbnail.Height") 175 | } 176 | if res.Media.Images.StandardResolution.Url != "https://scontent.cdninstagram.com/t51.2885-15/e15/11330516_482141235286676_1137904329_n.jpg?ig_cache_key=NTk0OTE0NzU4NDEyMTAzMzE1.2" { 177 | t.Error("Images.StandardResolution.Url") 178 | } 179 | if len(res.Media.UsersInPhoto) > 0 { 180 | t.Error("UsersInPhoto") 181 | } 182 | if res.Media.Caption.Text != "Welcome to the anti-stink zone." { 183 | t.Error("Caption.Text") 184 | } 185 | if tm, err := res.Media.Caption.CreatedTime.Time(); err != nil || tm.Unix() != 1385139387 { 186 | t.Error("Caption.CreatedTime") 187 | } 188 | if res.Media.Id != "594914758412103315_2134762" { 189 | t.Error("Id") 190 | } 191 | if res.Media.User.Username != "lululemon" { 192 | t.Error("User.Username") 193 | } 194 | } 195 | 196 | func TestGetMediaSearch(t *testing.T) { 197 | res, err := api.GetMediaSearch(values( 198 | "lat", "48.858844", 199 | "lng", "2.294351", 200 | "distance", "1000", // 1km 201 | )) 202 | checkRes(t, res.Meta, err) 203 | 204 | if len(res.Medias) == 0 { 205 | t.Error("Paris has to have more than 0 images taken in the last 5 days. Check for a nuclear device.") 206 | } 207 | } 208 | 209 | func TestGetMediaSearchError(t *testing.T) { 210 | res, err := api.GetMediaSearch(nil) 211 | if err == nil { 212 | t.Error("Error should have been thrown!", res) 213 | } else if err.Error() != "Error making api call: Code 400 APIInvalidParametersError missing lat and lng" { 214 | t.Error("Error isn't right!") 215 | } 216 | } 217 | 218 | func TestGetTag(t *testing.T) { 219 | res, err := api.GetTag("tbt", nil) // Throw Back Thursday #tbt 220 | checkRes(t, res.Meta, err) 221 | if res.Tag.Name != "tbt" { 222 | t.Error("Tag Name") 223 | } else if res.Tag.MediaCount < 120000000 { 224 | t.Error("Tag MediaCount", res.Tag.MediaCount) 225 | } 226 | } 227 | 228 | func TestGetTagRecentMedia(t *testing.T) { 229 | res, err := api.GetTagRecentMedia("tbt", values("count", "10")) 230 | checkRes(t, res.Meta, err) 231 | 232 | MediaLoop: 233 | for _, media := range res.Medias { 234 | for _, tag := range media.Tags { 235 | if tag == "tbt" { 236 | continue MediaLoop 237 | } 238 | } 239 | t.Error("No tbt tag on media", media.Id, media.Tags) 240 | } 241 | } 242 | 243 | func TestGetTagSearch(t *testing.T) { 244 | res, err := api.GetTagSearch(values("q", "toob")) 245 | checkRes(t, res.Meta, err) 246 | 247 | if len(res.Tags) != 46 { 248 | t.Error("Should be exact match", len(res.Tags)) 249 | } else if res.Tags[1].Name != "toob" { 250 | t.Error("Tag name should be exact match to query") 251 | } 252 | } 253 | 254 | func TestGetMediaLikes(t *testing.T) { 255 | res, err := api.GetMediaLikes("594914758412103315_2134762", nil) 256 | checkRes(t, res.Meta, err) 257 | 258 | if len(res.Users) < 10 { 259 | t.Error("too few likers!", len(res.Users)) 260 | } 261 | } 262 | 263 | func TestGetMediaComments(t *testing.T) { 264 | res, err := api.GetMediaComments("594914758412103315_2134762", nil) 265 | checkRes(t, res.Meta, err) 266 | 267 | if len(res.Comments) < 10 { 268 | t.Error("too few comments!", len(res.Comments)) 269 | } 270 | } 271 | 272 | func TestGetLocation(t *testing.T) { 273 | locationID := "285540617" 274 | locationName := "Tungkop, Cebu, Philippines" 275 | lat := 10.2419 276 | lng := 123.788 277 | 278 | res, err := api.GetLocation(locationID, nil) 279 | checkRes(t, res.Meta, err) 280 | 281 | loc := res.Location 282 | if ParseLocationId(loc.Id) != locationID { 283 | t.Error("location ID is wrong", loc.Id) 284 | } else if loc.Name != locationName { 285 | t.Error("location id and name don't match") 286 | } else if loc.Latitude != lat || loc.Longitude != lng { 287 | t.Error("Latitude and longitude are off", loc.Latitude, loc.Longitude) 288 | } 289 | } 290 | 291 | func TestGetLocationRecentMedia(t *testing.T) { 292 | res, err := api.GetLocationRecentMedia("249042610", nil) 293 | checkRes(t, res.Meta, err) 294 | 295 | if len(res.Medias) == 0 { 296 | t.Error("Should be at least one medias in count. We are talking about the Eiffel Tower", len(res.Medias)) 297 | } 298 | 299 | for _, media := range res.Medias { 300 | if media.Location.Name != "Eiffle Tower, Paris" { 301 | t.Error("Location in media isn't Eiffle Tower, Paris") 302 | } 303 | } 304 | } 305 | 306 | func TestGetLocationSearch(t *testing.T) { 307 | res, err := api.GetLocationSearch(values( 308 | "lat", "48.850111469312", 309 | "lng", "2.4040552046168", 310 | "distance", "1", // 1m 311 | )) 312 | checkRes(t, res.Meta, err) 313 | 314 | if len(res.Locations) == 0 { 315 | t.Error("Should be at least 1 location") 316 | } 317 | 318 | for _, loc := range res.Locations { 319 | if ParseLocationId(loc.Id) == "52655975" { 320 | if loc.Name != "La Parisienne" { 321 | t.Error("location id and name don't match") 322 | } else if loc.Latitude != 48.850111469312 || loc.Longitude != 2.4040552046168 { 323 | t.Error("Latitude and longitude are off", loc.Latitude, loc.Longitude) 324 | } 325 | return 326 | } 327 | } 328 | t.Error("La Parisienne isn't found!") 329 | } 330 | 331 | func TestGetUserFollows(t *testing.T) { 332 | res, err := api.GetUserFollows(nil) 333 | checkRes(t, res.Meta, err) 334 | 335 | if len(res.Users) == 0 { 336 | t.Error("You've been following ", len(res.Users)) 337 | } 338 | } 339 | 340 | func TestGetUserFollowsNonTrivial(t *testing.T) { 341 | res, err := api.GetUserFollows(nil) 342 | checkRes(t, res.Meta, err) 343 | 344 | if len(res.Users) == 0 { 345 | t.Error("You should have follow ", len(res.Users)) 346 | } 347 | } 348 | 349 | func TestGetUserFollowedBy(t *testing.T) { 350 | res, err := api.GetUserFollowedBy(nil) 351 | checkRes(t, res.Meta, err) 352 | 353 | if len(res.Users) == 0 { 354 | t.Error("You've been followed by ", len(res.Users)) 355 | } 356 | } 357 | 358 | func TestGetUserRequestedBy(t *testing.T) { 359 | authorizedRequest(t) 360 | 361 | res, err := api.GetUserRequestedBy(nil) 362 | checkRes(t, res.Meta, err) 363 | // not much to do here 364 | } 365 | 366 | func TestGetUserRelationship(t *testing.T) { 367 | authorizedRequest(t) 368 | 369 | res, err := api.GetUserRelationship(ladygaga_id, nil) 370 | checkRes(t, res.Meta, err) 371 | 372 | if res.Relationship.OutgoingStatus == "" { 373 | t.Error("OutgoingStatus should at least be none", res.Relationship.OutgoingStatus) 374 | } 375 | if res.Relationship.IncomingStatus == "" { 376 | t.Error("IncomingStatus should at least be none", res.Relationship.OutgoingStatus) 377 | } 378 | } 379 | 380 | // -- helpers -- 381 | 382 | func authorizedRequest(t *testing.T) { 383 | if !DoAuthorizedRequests { 384 | t.Skip("Access Token not provided.") 385 | } 386 | } 387 | 388 | func checkRes(t *testing.T, m *Meta, err error) { 389 | if err != nil { 390 | t.Error(err) 391 | } 392 | if m == nil || m.Code != 200 { 393 | t.Error("Meta not right", m) 394 | } 395 | } 396 | 397 | func values(keyValues ...string) url.Values { 398 | v := url.Values{} 399 | for i := 0; i < len(keyValues)-1; i += 2 { 400 | v.Set(keyValues[i], keyValues[i+1]) 401 | } 402 | return v 403 | } 404 | 405 | func createApi() *Api { 406 | return New(TestConfig["client_id"], TestConfig["client_secret"], TestConfig["access_token"], true) 407 | } 408 | --------------------------------------------------------------------------------