├── .gitignore ├── .travis.gofmt.sh ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── account.go ├── activity.go ├── broadcasts.go ├── challenge.go ├── cmds └── refreshTestAccounts │ └── main.go ├── collections.go ├── comments.go ├── const.go ├── contacts.go ├── docs ├── .gitkeep ├── _config.yml └── index.md ├── env.go ├── explore.go ├── feeds.go ├── generator.go ├── go.mod ├── go.sum ├── goinsta.go ├── hashtags.go ├── headless.go ├── igtv.go ├── inbox.go ├── location.go ├── media.go ├── profiles.go ├── request.go ├── search.go ├── shortid.go ├── stories.go ├── tests ├── account_test.go ├── feed_test.go ├── igtv_test.go ├── inbox_test.go ├── login_test.go ├── profile_test.go ├── search_test.go ├── shortid_test.go ├── sysacc_test.go ├── timeline_test.go └── upload_test.go ├── timeline.go ├── twofactor.go ├── types.go ├── uploads.go ├── users.go ├── utilities ├── abool.go ├── encryption.go └── totp.go ├── utils.go └── wrapper.go /.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 | *.test 24 | *.prof 25 | 26 | *.out 27 | *.bak 28 | *.jpg 29 | *.jpeg 30 | *.png 31 | 32 | # ignore Intellij products technical folder 33 | .idea/ 34 | 35 | .vscode/ 36 | .vim/ 37 | .tests/ 38 | .env 39 | examples/configs/ 40 | downloads/ 41 | 42 | notes.txt 43 | 44 | // Local file for testing 45 | local.go 46 | -------------------------------------------------------------------------------- /.travis.gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -n "$(gofmt -l .)" ]; then 3 | echo "GoInsta is not formatted !" 4 | gofmt -d . 5 | exit 1 6 | else 7 | echo "GoInsta is well formatted ;)" 8 | fi 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - master 5 | 6 | env: 7 | global: 8 | - GO111MODULE=off 9 | 10 | script: 11 | - chmod +x .travis.gofmt.sh 12 | - gofmt -d . 13 | - go vet -v ./... 14 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then go test -v ./... ; fi' 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | Welcome programmer! 4 | 5 | If you want to contribute to Goinsta API you must follow a simple instructions. 6 | 7 | - **Test your code after making pull request**. The title says it all. 8 | - **Include jokes if you can**. This instruction is optional. 9 | 10 | # Tests 11 | 12 | You need at least one goinsta exported object 13 | ``` 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/Davincible/goinsta/v3" 19 | "github.com/Davincible/goinsta/v3/utilities" 20 | ) 21 | 22 | func main() { 23 | inst := goinsta.New("user", "password") 24 | err := inst.Login() 25 | if err != nil { 26 | fmt.Fatal(err) 27 | } 28 | fmt.Print(utilities.ExportAsBase64String(inst)) 29 | } 30 | ``` 31 | 32 | Then you can use the output generated above to run your tests in the cli 33 | ``` 34 | INSTAGRAM_BASE64_USERNAME=BASE64_OUTPUT go test ./... 35 | ``` 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ahmadreza Zibaei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![goinsta logo](https://raw.githubusercontent.com/Davincible/goinsta/v1/resources/goinsta-image.png) 3 | 4 | [![GoDoc](https://godoc.org/github.com/Davincible/goinsta/v3?status.svg)](https://godoc.org/github.com/Davincible/goinsta/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/Davincible/goinsta/v3)](https://goreportcard.com/report/github.com/Davincible/goinsta/v3) 5 | 6 | ## Go Instagram Private API 7 | 8 | > Unofficial Instagram API for Golang 9 | 10 | This repository has been forked from [ahmdrz/goinsta](https://github.com/ahmdrz/goinsta). 11 | As the maintainer of this repositry has archived the project, and 12 | the code in the repository was based on a few year old instagram app version, 13 | since which a lot has changed, I have taken the courtesy to build upon his 14 | great framework and update the code to be compatible with apk v250.0.0.21.109 15 | (Aug 30, 2022). Walkthrough docs can be found in the 16 | [wiki](https://github.com/Davincible/goinsta/wiki/1.-Getting-Started). 17 | 18 | If you are missing anything or something is not working as expected please let 19 | me know through the issues or discussions. 20 | 21 | ### Features 22 | 23 | * **HTTP2 by default. Goinsta uses HTTP2 client enhancing performance.** 24 | * **Object independency. Can handle multiple instagram accounts.** 25 | * **Like Instagram mobile application**. Goinsta is very similar to Instagram official application. 26 | * **Simple**. Goinsta is made by lazy programmers! 27 | * **Backup methods**. You can use Export`and Import`functions. 28 | * **Security**. Your password is only required to login. After login your password is deleted. 29 | * ~~**No External Dependencies**. GoInsta will not use any Go packages outside of the standard library.~~ goinsta now uses [chromedp](https://github.com/chromedp/chromedp) as headless browser driver to solve challanges and checkpoints. 30 | 31 | ### Package installation 32 | 33 | `go get -u github.com/Davincible/goinsta/v3@latest` 34 | 35 | ### Example 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | 43 | "github.com/Davincible/goinsta/v3" 44 | ) 45 | 46 | func main() { 47 | insta := goinsta.New("USERNAME", "PASSWORD") 48 | 49 | // Only call Login the first time you login. Next time import your config 50 | if err := insta.Login(); err != nil { 51 | panic(err) 52 | } 53 | 54 | // Export your configuration 55 | // after exporting you can use Import function instead of New function. 56 | // insta, err := goinsta.Import("~/.goinsta") 57 | // it's useful when you want use goinsta repeatedly. 58 | // Export is deffered because every run insta should be exported at the end of the run 59 | // as the header cookies change constantly. 60 | defer insta.Export("~/.goinsta") 61 | 62 | ... 63 | } 64 | ``` 65 | 66 | For the full documentation, check the [wiki](https://github.com/Davincible/goinsta/wiki/01.-Getting-Started), or run `go doc -all`. 67 | 68 | ### Legal 69 | 70 | This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by Instagram or any of its affiliates or subsidiaries. This is an independent and unofficial API. Use at your own risk. 71 | 72 | -------------------------------------------------------------------------------- /activity.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Activity is the recent activity menu. 9 | // 10 | // See example: examples/activity/recent.go 11 | type Activity struct { 12 | insta *Instagram 13 | err error 14 | 15 | // Ad is every column of Activity section 16 | Ad struct { 17 | Items []struct { 18 | // User User `json:"user"` 19 | Algorithm string `json:"algorithm"` 20 | SocialContext string `json:"social_context"` 21 | Icon string `json:"icon"` 22 | Caption string `json:"caption"` 23 | MediaIds []interface{} `json:"media_ids"` 24 | ThumbnailUrls []interface{} `json:"thumbnail_urls"` 25 | LargeUrls []interface{} `json:"large_urls"` 26 | MediaInfos []interface{} `json:"media_infos"` 27 | Value float64 `json:"value"` 28 | IsNewSuggestion bool `json:"is_new_suggestion"` 29 | } `json:"items"` 30 | MoreAvailable bool `json:"more_available"` 31 | } `json:"aymf"` 32 | Counts struct { 33 | Campaign int `json:"campaign_notification"` 34 | CommentLikes int `json:"comment_likes"` 35 | Comments int `json:"comments"` 36 | Fundraiser int `json:"fundraiser"` 37 | Likes int `json:"likes"` 38 | NewPosts int `json:"new_posts"` 39 | PhotosOfYou int `json:"photos_of_you"` 40 | Relationships int `json:"relationships"` 41 | Requests int `json:"requests"` 42 | Shopping int `json:"shopping_notification"` 43 | UserTags int `json:"usertags"` 44 | } `json:"counts"` 45 | FriendRequestStories []interface{} `json:"friend_request_stories"` 46 | NewStories []RecentItems `json:"new_stories"` 47 | OldStories []RecentItems `json:"old_stories"` 48 | ContinuationToken int64 `json:"continuation_token"` 49 | Subscription interface{} `json:"subscription"` 50 | NextID string `json:"next_max_id"` 51 | LastChecked float64 `json:"last_checked"` 52 | FirstRecTs float64 `json:"pagination_first_record_timestamp"` 53 | 54 | Status string `json:"status"` 55 | } 56 | 57 | type RecentItems struct { 58 | Type int `json:"type"` 59 | StoryType int `json:"story_type"` 60 | Args struct { 61 | Text string `json:"text"` 62 | RichText string `json:"rich_text"` 63 | IconUrl string `json:"icon_url"` 64 | Links []struct { 65 | Start int `json:"start"` 66 | End int `json:"end"` 67 | Type string `json:"type"` 68 | ID interface{} `json:"id"` 69 | } `json:"links"` 70 | InlineFollow struct { 71 | UserInfo User `json:"user_info"` 72 | Following bool `json:"following"` 73 | OutgoingRequest bool `json:"outgoing_request"` 74 | } `json:"inline_follow"` 75 | Actions []string `json:"actions"` 76 | AfCandidateId int `json:"af_candidate_id"` 77 | ProfileID int64 `json:"profile_id"` 78 | ProfileImage string `json:"profile_image"` 79 | Timestamp float64 `json:"timestamp"` 80 | Tuuid string `json:"tuuid"` 81 | Clicked bool `json:"clicked"` 82 | ProfileName string `json:"profile_name"` 83 | LatestReelMedia int64 `json:"latest_reel_media"` 84 | Destination string `json:"destination"` 85 | Extra interface{} `json:"extra"` 86 | } `json:"args"` 87 | Counts struct{} `json:"counts"` 88 | Pk string `json:"pk"` 89 | } 90 | 91 | func (act *Activity) Error() error { 92 | return act.err 93 | } 94 | 95 | // Next function allows pagination over notifications. 96 | // 97 | // See example: examples/activity/recent.go 98 | func (act *Activity) Next() bool { 99 | if act.err != nil { 100 | return false 101 | } 102 | var first bool 103 | if act.Status == "" { 104 | first = true 105 | } 106 | 107 | query := map[string]string{ 108 | "mark_as_seen": "false", 109 | "timezone_offset": timeOffset, 110 | } 111 | if act.NextID != "" { 112 | query["max_id"] = act.NextID 113 | query["last_checked"] = fmt.Sprintf("%f", act.LastChecked) 114 | query["pagination_first_record_timestamp"] = fmt.Sprintf("%f", act.FirstRecTs) 115 | } 116 | 117 | insta := act.insta 118 | body, _, err := insta.sendRequest( 119 | &reqOptions{ 120 | Endpoint: urlActivityRecent, 121 | Query: query, 122 | }, 123 | ) 124 | if err != nil { 125 | act.err = err 126 | return false 127 | } 128 | 129 | act2 := Activity{} 130 | err = json.Unmarshal(body, &act2) 131 | if err == nil { 132 | *act = act2 133 | act.insta = insta 134 | if first { 135 | if err := act.MarkAsSeen(); err != nil { 136 | act.err = err 137 | return false 138 | } 139 | } 140 | 141 | if act.NextID == "" { 142 | act.err = ErrNoMore 143 | return false 144 | } 145 | return true 146 | } 147 | act.err = err 148 | return false 149 | } 150 | 151 | // MarkAsSeen will let instagram know you visited the activity page, and mark 152 | // current items as seen. 153 | func (act *Activity) MarkAsSeen() error { 154 | insta := act.insta 155 | _, _, err := insta.sendRequest( 156 | &reqOptions{ 157 | Endpoint: urlActivitySeen, 158 | IsPost: true, 159 | Query: map[string]string{ 160 | "_uuid": insta.uuid, 161 | }, 162 | }, 163 | ) 164 | return err 165 | } 166 | 167 | func newActivity(insta *Instagram) *Activity { 168 | act := &Activity{ 169 | insta: insta, 170 | } 171 | return act 172 | } 173 | -------------------------------------------------------------------------------- /broadcasts.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | // Broadcast struct represents live video streams. 11 | type Broadcast struct { 12 | insta *Instagram 13 | // Mutex to keep track of BroadcastStatus 14 | mu *sync.RWMutex 15 | 16 | LastLikeTs int64 17 | LastCommentTs int64 18 | LastCommentFetchTs int64 19 | LastCommentTotal int 20 | 21 | ID int64 `json:"id"` 22 | MediaID string `json:"media_id"` 23 | LivePostID int64 `json:"live_post_id"` 24 | 25 | // BroadcastStatus is either "active", "interrupted", "stopped" 26 | BroadcastStatus string `json:"broadcast_status"` 27 | DashPlaybackURL string `json:"dash_playback_url"` 28 | DashAbrPlaybackURL string `json:"dash_abr_playback_url"` 29 | DashManifest string `json:"dash_manifest"` 30 | ExpireAt int64 `json:"expire_at"` 31 | EncodingTag string `json:"encoding_tag"` 32 | InternalOnly bool `json:"internal_only"` 33 | NumberOfQualities int `json:"number_of_qualities"` 34 | CoverFrameURL string `json:"cover_frame_url"` 35 | User User `json:"broadcast_owner"` 36 | Cobroadcasters []*User `json:"cobroadcasters"` 37 | PublishedTime int64 `json:"published_time"` 38 | Message string `json:"broadcast_message"` 39 | OrganicTrackingToken string `json:"organic_tracking_token"` 40 | IsPlayerLiveTrace int `json:"is_player_live_trace_enabled"` 41 | IsGamingContent bool `json:"is_gaming_content"` 42 | IsViewerCommentAllowed bool `json:"is_viewer_comment_allowed"` 43 | IsPolicyViolation bool `json:"is_policy_violation"` 44 | PolicyViolationReason string `json:"policy_violation_reason"` 45 | LiveCommentMentionEnabled bool `json:"is_live_comment_mention_enabled"` 46 | LiveCommentRepliesEnabled bool `json:"is_live_comment_replies_enabled"` 47 | HideFromFeedUnit bool `json:"hide_from_feed_unit"` 48 | VideoDuration float64 `json:"video_duration"` 49 | Visibility int `json:"visibility"` 50 | ViewerCount float64 `json:"viewer_count"` 51 | ResponseTs int64 `json:"response_timestamp"` 52 | Status string `json:"status"` 53 | Dimensions struct { 54 | Width int `json:"width"` 55 | Height int `json:"height"` 56 | } `json:"dimensions"` 57 | Experiments map[string]interface{} `json:"broadcast_experiments"` 58 | PayViewerConfig struct { 59 | PayConfig struct { 60 | ConsumptionSheetConfig struct { 61 | Description string `json:"description"` 62 | PrivacyDisclaimer string `json:"privacy_disclaimer"` 63 | PrivacyDisclaimerLink string `json:"privacy_disclaimer_link"` 64 | PrivacyDisclaimerLinkText string `json:"privacy_disclaimer_link_text"` 65 | } `json:"consumption_sheet_config"` 66 | DigitalNonConsumableProductID int64 `json:"digital_non_consumable_product_id"` 67 | DigitalProductID int64 `json:"digital_product_id"` 68 | PayeeID int64 `json:"payee_id"` 69 | PinnedRowConfig struct { 70 | ButtonTitle string `json:"button_title"` 71 | Description string `json:"description"` 72 | } `json:"pinned_row_config"` 73 | TierInfos []struct { 74 | DigitalProductID int64 `json:"digital_product_id"` 75 | Sku string `json:"sku"` 76 | SupportTier string `json:"support_tier"` 77 | } `json:"tier_infos"` 78 | } `json:"pay_config"` 79 | } `json:"user_pay_viewer_config"` 80 | } 81 | 82 | type BroadcastComments struct { 83 | CommentLikesEnabled bool `json:"comment_likes_enabled"` 84 | Comments []*Comment `json:"comments"` 85 | PinnedComment *Comment `json:"pinned_comment"` 86 | CommentCount int `json:"comment_count"` 87 | Caption *Caption `json:"caption"` 88 | CaptionIsEdited bool `json:"caption_is_edited"` 89 | HasMoreComments bool `json:"has_more_comments"` 90 | HasMoreHeadloadComments bool `json:"has_more_headload_comments"` 91 | MediaHeaderDisplay string `json:"media_header_display"` 92 | CanViewMorePreviewComments bool `json:"can_view_more_preview_comments"` 93 | LiveSecondsPerComment int `json:"live_seconds_per_comment"` 94 | IsFirstFetch string `json:"is_first_fetch"` 95 | SystemComments []*Comment `json:"system_comments"` 96 | CommentMuted int `json:"comment_muted"` 97 | IsViewerCommentAllowed bool `json:"is_viewer_comment_allowed"` 98 | UserPaySupportersInfo struct { 99 | SupportersInComments map[string]interface{} `json:"supporters_in_comments"` 100 | SupportersInCommentsV2 map[string]interface{} `json:"supporters_in_comments_v2"` 101 | // SupportersInCommentsV2 map[string]struct { 102 | // SupportTier string `json:"support_tier"` 103 | // BadgesCount int `json:"badges_count"` 104 | // } `json:"supporters_in_comments_v2"` 105 | NewSupportersNextMinID int64 `json:"new_supporters_next_min_id"` 106 | NewSupporters []NewSupporter `json:"new_supporters"` 107 | } `json:"user_pay_supporter_info"` 108 | Status string `json:"status"` 109 | } 110 | 111 | type NewSupporter struct { 112 | RepeatedSupporter bool `json:"is_repeat_supporter"` 113 | SupportTier string `json:"support_tier"` 114 | Timestamp float64 `json:"ts_secs"` 115 | User struct { 116 | ID int64 `json:"pk"` 117 | Username string `json:"username"` 118 | FullName string `json:"full_name"` 119 | IsPrivate bool `json:"is_private"` 120 | IsVerified bool `json:"is_verified"` 121 | } 122 | } 123 | 124 | type BroadcastLikes struct { 125 | Likes int `json:"likes"` 126 | BurstLikes int `json:"burst_likes"` 127 | Likers []struct { 128 | UserID int64 `json:"user_id"` 129 | ProfilePicUrl string `json:"profile_pic_url"` 130 | Count string `json:"count"` 131 | } `json:"likers"` 132 | LikeTs int64 `json:"like_ts"` 133 | Status string `json:"status"` 134 | PaySupporterInfo struct { 135 | LikeCountByTier []struct { 136 | BurstLikes int `json:"burst_likes"` 137 | Likers []interface{} `json:"likers"` 138 | Likes int `json:"likes"` 139 | SupportTier string `json:"support_tier"` 140 | } `json:"like_count_by_support_tier"` 141 | BurstLikes int `json:"supporter_tier_burst_likes"` 142 | Likes int `json:"supporter_tier_likes"` 143 | } `json:"user_pay_supporter_info"` 144 | } 145 | 146 | type BroadcastHeartbeat struct { 147 | ViewerCount float64 `json:"viewer_count"` 148 | BroadcastStatus string `json:"broadcast_status"` 149 | CobroadcasterIds []string `json:"cobroadcaster_ids"` 150 | OffsetVideoStart float64 `json:"offset_to_video_start"` 151 | RequestToJoinEnabled int `json:"request_to_join_enabled"` 152 | UserPayMaxAmountReached bool `json:"user_pay_max_amount_reached"` 153 | Status string `json:"status"` 154 | } 155 | 156 | // Discover wraps Instagram.IGTV.Live 157 | func (br *Broadcast) Discover() (*IGTVChannel, error) { 158 | return br.insta.IGTV.Live() 159 | } 160 | 161 | // NewUser returns prepared user to be used with his functions. 162 | func (insta *Instagram) NewBroadcast(id int64) *Broadcast { 163 | return &Broadcast{insta: insta, ID: id, mu: &sync.RWMutex{}} 164 | } 165 | 166 | // GetInfo will fetch the information about a broadcast 167 | func (br *Broadcast) GetInfo() error { 168 | body, _, err := br.insta.sendRequest( 169 | &reqOptions{ 170 | Endpoint: fmt.Sprintf(urlLiveInfo, br.ID), 171 | Query: map[string]string{ 172 | "view_expired_broadcast": "false", 173 | }, 174 | }) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | br.mu.RLock() 180 | err = json.Unmarshal(body, br) 181 | br.mu.RUnlock() 182 | return err 183 | } 184 | 185 | // Call every 2 seconds 186 | func (br *Broadcast) GetComments() (*BroadcastComments, error) { 187 | br.mu.RLock() 188 | if br.BroadcastStatus == "stopped" { 189 | return nil, ErrMediaDeleted 190 | } 191 | br.mu.RUnlock() 192 | 193 | body, _, err := br.insta.sendRequest( 194 | &reqOptions{ 195 | Endpoint: fmt.Sprintf(urlLiveComments, br.ID), 196 | Query: map[string]string{ 197 | "last_comment_ts": strconv.FormatInt(br.LastCommentTs, 10), 198 | "join_request_last_seen_ts": "0", 199 | "join_request_last_fetch_ts": "0", 200 | "join_request_last_total_count": "0", 201 | }, 202 | }) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | c := &BroadcastComments{} 208 | err = json.Unmarshal(body, c) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | if c.CommentCount > 0 { 214 | br.LastCommentTs = c.Comments[0].CreatedAt 215 | } 216 | return c, nil 217 | } 218 | 219 | // Call every 6 seconds 220 | func (br *Broadcast) GetLikes() (*BroadcastLikes, error) { 221 | br.mu.RLock() 222 | if br.BroadcastStatus == "stopped" { 223 | return nil, ErrMediaDeleted 224 | } 225 | br.mu.RUnlock() 226 | 227 | body, _, err := br.insta.sendRequest( 228 | &reqOptions{ 229 | Endpoint: fmt.Sprintf(urlLiveLikeCount, br.ID), 230 | Query: map[string]string{ 231 | "like_ts": strconv.FormatInt(br.LastLikeTs, 10), 232 | }, 233 | }) 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | c := &BroadcastLikes{} 239 | err = json.Unmarshal(body, c) 240 | if err != nil { 241 | return nil, err 242 | } 243 | br.LastLikeTs = c.LikeTs 244 | return c, nil 245 | } 246 | 247 | // Call every 3 seconds 248 | func (br *Broadcast) GetHeartbeat() (*BroadcastHeartbeat, error) { 249 | br.mu.RLock() 250 | if br.BroadcastStatus == "stopped" { 251 | return nil, ErrMediaDeleted 252 | } 253 | br.mu.RUnlock() 254 | 255 | body, _, err := br.insta.sendRequest( 256 | &reqOptions{ 257 | Endpoint: fmt.Sprintf(urlLiveHeartbeat, br.ID), 258 | IsPost: true, 259 | Query: map[string]string{ 260 | "_uuid": br.insta.uuid, 261 | "live_with_eligibility": "2", // What is this? 262 | }, 263 | }) 264 | if err != nil { 265 | return nil, err 266 | } 267 | 268 | c := &BroadcastHeartbeat{} 269 | err = json.Unmarshal(body, c) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | br.mu.RLock() 275 | br.BroadcastStatus = c.BroadcastStatus 276 | br.mu.RUnlock() 277 | 278 | return c, nil 279 | } 280 | 281 | // GetLiveChaining traditionally gets called after the live stream has ended, and provides 282 | // recommendations of other current live streams, as well as past live streams. 283 | func (br *Broadcast) GetLiveChaining() ([]*Broadcast, error) { 284 | insta := br.insta 285 | body, _, err := insta.sendRequest( 286 | &reqOptions{ 287 | Endpoint: urlLiveChaining, 288 | Query: map[string]string{ 289 | "include_post_lives": "true", 290 | }, 291 | }, 292 | ) 293 | if err != nil { 294 | return nil, err 295 | } 296 | 297 | var resp struct { 298 | Broadcasts []*Broadcast `json:"broadcasts"` 299 | Status string `json:"string"` 300 | } 301 | err = json.Unmarshal(body, &resp) 302 | if err != nil { 303 | return nil, err 304 | } 305 | for _, br := range resp.Broadcasts { 306 | br.setValues(insta) 307 | } 308 | return resp.Broadcasts, nil 309 | } 310 | 311 | func (br *Broadcast) DownloadCoverFrame() ([]byte, error) { 312 | if br.CoverFrameURL == "" { 313 | return nil, ErrNoMedia 314 | } 315 | 316 | b, err := br.insta.download(br.CoverFrameURL) 317 | if err != nil { 318 | return nil, err 319 | } 320 | return b, nil 321 | } 322 | 323 | func (br *Broadcast) setValues(insta *Instagram) { 324 | br.insta = insta 325 | br.User.insta = insta 326 | br.mu = &sync.RWMutex{} 327 | for _, cb := range br.Cobroadcasters { 328 | cb.insta = insta 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /challenge.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type ChallengeStepData struct { 9 | Choice string `json:"choice"` 10 | FbAccessToken string `json:"fb_access_token"` 11 | BigBlueToken string `json:"big_blue_token"` 12 | GoogleOauthToken string `json:"google_oauth_token"` 13 | Email string `json:"email"` 14 | SecurityCode string `json:"security_code"` 15 | ResendDelay interface{} `json:"resend_delay"` 16 | ContactPoint string `json:"contact_point"` 17 | FormType string `json:"form_type"` 18 | } 19 | 20 | // Challenge is a status code 400 error, usually prompting the user to perform 21 | // some action. 22 | type Challenge struct { 23 | insta *Instagram 24 | 25 | LoggedInUser *Account `json:"logged_in_user,omitempty"` 26 | UserID int64 `json:"user_id"` 27 | Status string `json:"status"` 28 | 29 | Errors []string `json:"errors"` 30 | 31 | URL string `json:"url"` 32 | ApiPath string `json:"api_path"` 33 | Context *ChallengeContext `json:"challenge_context"` 34 | FlowRenderType int `json:"flow_render_type"` 35 | HideWebviewHeader bool `json:"hide_webview_header"` 36 | Lock bool `json:"lock"` 37 | Logout bool `json:"logout"` 38 | NativeFlow bool `json:"native_flow"` 39 | 40 | TwoFactorRequired bool 41 | TwoFactorInfo TwoFactorInfo 42 | } 43 | 44 | // Checkpoint is just like challenge, a status code 400 error. 45 | // Usually used to prompt the user to accept cookies. 46 | type Checkpoint struct { 47 | insta *Instagram 48 | 49 | URL string `json:"checkpoint_url"` 50 | Lock bool `json:"lock"` 51 | FlowRenderType int `json:"flow_render_type"` 52 | } 53 | 54 | type ChallengeContext struct { 55 | TypeEnum string `json:"challenge_type_enum"` 56 | IsStateless bool `json:"is_stateless"` 57 | Action string `json:"action"` 58 | NonceCode string `json:"nonce_code"` 59 | StepName string `json:"step_name"` 60 | StepData ChallengeStepData `json:"step_data"` 61 | UserID int64 `json:"user_id"` 62 | } 63 | 64 | type challengeResp struct { 65 | *Challenge 66 | } 67 | 68 | func newChallenge(insta *Instagram) *Challenge { 69 | return &Challenge{ 70 | insta: insta, 71 | } 72 | } 73 | 74 | // updateState updates current data from challenge url 75 | func (c *Challenge) updateState() error { 76 | insta := c.insta 77 | 78 | ctx, err := json.Marshal(c.Context) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | body, _, err := insta.sendRequest( 84 | &reqOptions{ 85 | Endpoint: c.insta.challengeURL, 86 | Query: map[string]string{ 87 | "guid": insta.uuid, 88 | "device_id": insta.dID, 89 | "challenge_context": string(ctx), 90 | }, 91 | }, 92 | ) 93 | if err == nil { 94 | resp := challengeResp{} 95 | err = json.Unmarshal(body, &resp) 96 | if err == nil { 97 | *c = *resp.Challenge 98 | c.insta = insta 99 | } 100 | } 101 | return err 102 | } 103 | 104 | // selectVerifyMethod selects a way and verify it (Phone number = 0, email = 1) 105 | func (challenge *Challenge) selectVerifyMethod(choice string, isReplay ...bool) error { 106 | insta := challenge.insta 107 | 108 | url := challenge.insta.challengeURL 109 | if len(isReplay) > 0 && isReplay[0] { 110 | url = strings.Replace(url, "/challenge/", "/challenge/replay/", -1) 111 | } 112 | 113 | data, err := json.Marshal( 114 | map[string]string{ 115 | "choice": choice, 116 | "guid": insta.uuid, 117 | "device_id": insta.dID, 118 | "_uuid": insta.uuid, 119 | "_uid": toString(insta.Account.ID), 120 | }) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | body, _, err := insta.sendRequest( 126 | &reqOptions{ 127 | Endpoint: url, 128 | Query: generateSignature(data), 129 | IsPost: true, 130 | }, 131 | ) 132 | if err == nil { 133 | resp := challengeResp{} 134 | err = json.Unmarshal(body, &resp) 135 | if err == nil { 136 | *challenge = *resp.Challenge 137 | challenge.insta = insta 138 | } 139 | } 140 | return err 141 | } 142 | 143 | // sendSecurityCode sends the code received in the message 144 | func (challenge *Challenge) SendSecurityCode(code string) error { 145 | insta := challenge.insta 146 | url := challenge.insta.challengeURL 147 | 148 | data, err := json.Marshal(map[string]string{ 149 | "security_code": code, 150 | "guid": insta.uuid, 151 | "device_id": insta.dID, 152 | "_uuid": insta.uuid, 153 | "_uid": toString(insta.Account.ID), 154 | }) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | body, _, err := insta.sendRequest( 160 | &reqOptions{ 161 | Endpoint: url, 162 | IsPost: true, 163 | Query: generateSignature(data), 164 | }, 165 | ) 166 | if err == nil { 167 | resp := challengeResp{} 168 | err = json.Unmarshal(body, &resp) 169 | if err == nil { 170 | *challenge = *resp.Challenge 171 | challenge.insta = insta 172 | challenge.LoggedInUser.insta = insta 173 | insta.Account = challenge.LoggedInUser 174 | } 175 | } 176 | return err 177 | } 178 | 179 | // deltaLoginReview process with choice (It was me = 0, It wasn't me = 1) 180 | func (c *Challenge) deltaLoginReview() error { 181 | return c.selectVerifyMethod("0") 182 | } 183 | 184 | func (c *Challenge) ProcessOld(apiURL string) error { 185 | c.insta.challengeURL = apiURL[1:] 186 | 187 | if err := c.updateState(); err != nil { 188 | return err 189 | } 190 | 191 | switch c.Context.StepName { 192 | case "select_verify_method": 193 | return c.selectVerifyMethod(c.Context.StepData.Choice) 194 | case "delta_login_review": 195 | return c.deltaLoginReview() 196 | } 197 | 198 | return ErrChallengeProcess{StepName: c.Context.StepName} 199 | } 200 | 201 | // Process will open up the challenge url in a chromium browser and 202 | // take a screenshot. Please report the screenshot and printed out struct so challenge 203 | // automation can be build in. 204 | func (c *Challenge) Process() error { 205 | insta := c.insta 206 | 207 | insta.warnHandler("Encountered a captcha challenge, goinsta will attempt to open the challenge in a headless chromium browser, and take a screenshot. Please report the details in a github issue.") 208 | err := insta.openChallenge(c.URL) 209 | err = checkHeadlessErr(err) 210 | 211 | return err 212 | } 213 | 214 | // Process will open up the url passed as a checkpoint response (not a challenge) 215 | // in a headless browser. This method is experimental, please report if you still 216 | // get a /privacy/checks/ checkpoint error. 217 | func (c *Checkpoint) Process() error { 218 | insta := c.insta 219 | if insta.privacyRequested.Get() { 220 | panic("Privacy request again, it hus failed, panicing") 221 | } 222 | 223 | insta.privacyRequested.Set(true) 224 | err := insta.acceptPrivacyCookies(c.URL) 225 | err = checkHeadlessErr(err) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | insta.privacyCalled.Set(true) 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /cmds/refreshTestAccounts/main.go: -------------------------------------------------------------------------------- 1 | // 2 | // Use this program to parse instagram accounts from goinsta/tests/.env, and 3 | // create base64 encoded configs that can be used for testing. 4 | // 5 | // To add accounts, add to tests/.env: INSTAGRAM_ACT_=":" 6 | // 7 | // Also make sure to add a pixabay api key under PIXABAY_API_KEY="" as 8 | // this is needed for some upload tests. 9 | // 10 | package main 11 | 12 | import ( 13 | "github.com/Davincible/goinsta/v3" 14 | ) 15 | 16 | func main() { 17 | // Open File 18 | path := "../../tests/.env" 19 | err := goinsta.EnvProvision(path) 20 | if err != nil { 21 | panic(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /comments.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Comments allows user to interact with media (item) comments. 12 | // You can Add or Delete by index or by user name. 13 | type Comments struct { 14 | item *Item 15 | endpoint string 16 | err error 17 | 18 | Items []Comment `json:"comments"` 19 | CommentCount int64 `json:"comment_count"` 20 | Caption Caption `json:"caption"` 21 | CaptionIsEdited bool `json:"caption_is_edited"` 22 | HasMoreComments bool `json:"has_more_comments"` 23 | HasMoreHeadloadComments bool `json:"has_more_headload_comments"` 24 | ThreadingEnabled bool `json:"threading_enabled"` 25 | MediaHeaderDisplay string `json:"media_header_display"` 26 | InitiateAtTop bool `json:"initiate_at_top"` 27 | InsertNewCommentToTop bool `json:"insert_new_comment_to_top"` 28 | PreviewComments []Comment `json:"preview_comments"` 29 | NextID json.RawMessage `json:"next_max_id,omitempty"` 30 | NextMinID json.RawMessage `json:"next_min_id,omitempty"` 31 | CommentLikesEnabled bool `json:"comment_likes_enabled"` 32 | DisplayRealtimeTypingIndicator bool `json:"display_realtime_typing_indicator"` 33 | Status string `json:"status"` 34 | } 35 | 36 | func (comments *Comments) setValues() { 37 | for i := range comments.Items { 38 | comments.Items[i].item = comments.item 39 | comments.Items[i].setValues(comments.item.insta) 40 | } 41 | } 42 | 43 | func newComments(item *Item) *Comments { 44 | c := &Comments{ 45 | item: item, 46 | } 47 | return c 48 | } 49 | 50 | func (comments Comments) Error() error { 51 | return comments.err 52 | } 53 | 54 | // Disable disables comments in FeedMedia. 55 | // 56 | // See example: examples/media/commentDisable.go 57 | func (comments *Comments) Disable() error { 58 | return comments.toggleComments(urlCommentDisable) 59 | } 60 | 61 | // Enable enables comments in FeedMedia 62 | // 63 | // See example: examples/media/commentEnable.go 64 | func (comments *Comments) Enable() error { 65 | return comments.toggleComments(urlCommentEnable) 66 | } 67 | 68 | func (comments *Comments) toggleComments(endpoint string) error { 69 | // Use something else to repel stories 70 | // switch comments.item.media.(type) { 71 | // case *StoryMedia: 72 | // return fmt.Errorf("Incompatible type. Cannot use Enable() with StoryMedia type") 73 | // } 74 | insta := comments.item.insta 75 | 76 | _, _, err := insta.sendRequest( 77 | &reqOptions{ 78 | Endpoint: fmt.Sprintf(endpoint, comments.item.ID), 79 | Query: map[string]string{"_uuid": insta.uuid}, 80 | IsPost: true, 81 | }, 82 | ) 83 | return err 84 | } 85 | 86 | // Next allows comment pagination. 87 | // 88 | // This function support concurrency methods to get comments using Last and Next ID 89 | // 90 | // New comments are stored in Comments.Items 91 | func (comments *Comments) Next() bool { 92 | if comments.err != nil { 93 | return false 94 | } 95 | 96 | item := comments.item 97 | insta := item.insta 98 | endpoint := comments.endpoint 99 | query := map[string]string{ 100 | // "can_support_threading": "true", 101 | } 102 | if comments.NextID != nil { 103 | next, _ := strconv.Unquote(string(comments.NextID)) 104 | query["max_id"] = next 105 | } else if comments.NextMinID != nil { 106 | next, _ := strconv.Unquote(string(comments.NextMinID)) 107 | query["min_id"] = next 108 | } 109 | 110 | body, _, err := insta.sendRequest( 111 | &reqOptions{ 112 | Endpoint: endpoint, 113 | Connection: "keep-alive", 114 | Query: query, 115 | }, 116 | ) 117 | if err == nil { 118 | c := Comments{} 119 | err = json.Unmarshal(body, &c) 120 | if err == nil { 121 | *comments = c 122 | comments.endpoint = endpoint 123 | comments.item = item 124 | if (!comments.HasMoreComments || comments.NextID == nil) && 125 | (!comments.HasMoreHeadloadComments || comments.NextMinID == nil) { 126 | comments.err = ErrNoMore 127 | } 128 | comments.setValues() 129 | return true 130 | } 131 | } 132 | comments.err = err 133 | return false 134 | } 135 | 136 | // Sync prepare Comments to receive comments. 137 | // Use Next to receive comments. 138 | // 139 | // See example: examples/media/commentsSync.go 140 | func (comments *Comments) Sync() { 141 | endpoint := fmt.Sprintf(urlCommentSync, comments.item.ID) 142 | comments.endpoint = endpoint 143 | } 144 | 145 | // Add push a comment in media. 146 | // 147 | // See example: examples/media/commentsAdd.go 148 | func (comments *Comments) Add(text string) (err error) { 149 | return comments.item.Comment(text) 150 | } 151 | 152 | // Delete deletes a single comment. 153 | func (c *Comment) Delete() error { 154 | return c.item.insta.bulkDelComments([]*Comment{c}) 155 | } 156 | 157 | // BulkDelete allows you to select and delete multiple comments on a single post. 158 | func (comments *Comments) BulkDelete(c []*Comment) error { 159 | return comments.item.insta.bulkDelComments(c) 160 | } 161 | 162 | func (insta *Instagram) bulkDelComments(c []*Comment) error { 163 | if len(c) == 0 { 164 | return nil 165 | } 166 | cIDs := toString(c[0].ID) 167 | pID := c[0].item.ID 168 | if len(c) > 1 { 169 | for _, i := range c[1:] { 170 | if i.item.ID != pID { 171 | return errors.New("All comments have to belong to the same post") 172 | } 173 | cIDs += "," + toString(i.ID) 174 | } 175 | } 176 | 177 | data, err := json.Marshal( 178 | map[string]string{ 179 | "comment_ids_to_delete": cIDs, 180 | "_uid": toString(insta.Account.ID), 181 | "_uuid": insta.uuid, 182 | "container_module": "feed_timeline", 183 | }, 184 | ) 185 | if err != nil { 186 | return err 187 | } 188 | _, _, err = insta.sendRequest( 189 | &reqOptions{ 190 | Endpoint: fmt.Sprintf(urlCommentBulkDelete, pID), 191 | Query: generateSignature(data), 192 | IsPost: true, 193 | }, 194 | ) 195 | return err 196 | } 197 | 198 | // DeleteMine removes all of your comments limited by parsed parameter. 199 | // 200 | // If limit is <= 0 DeleteMine will delete all your comments. 201 | // Be careful with using this on posts with a large number of comments, 202 | // as a large number of requests will be made to index all comments.This 203 | // can result in a ratelimiter being tripped. 204 | // 205 | // See example: examples/media/commentsDelMine.go 206 | func (comments *Comments) DeleteMine(limit int) error { 207 | comments.Sync() 208 | cList := make([]*Comment, 1) 209 | 210 | insta := comments.item.insta 211 | floop: 212 | for i := 0; comments.Next(); i++ { 213 | for _, c := range comments.Items { 214 | if c.UserID == insta.Account.ID || c.User.ID == insta.Account.ID { 215 | if limit > 0 && i >= limit { 216 | break floop 217 | } 218 | cList = append(cList, &c) 219 | } 220 | } 221 | time.Sleep(100 * time.Millisecond) 222 | } 223 | if err := comments.Error(); err != nil && err != ErrNoMore { 224 | return err 225 | } 226 | err := comments.BulkDelete(cList) 227 | return err 228 | } 229 | 230 | // Comment is a type of Media retrieved by the Comments methods 231 | type Comment struct { 232 | insta *Instagram 233 | item *Item 234 | 235 | ID interface{} `json:"pk"` 236 | Text string `json:"text"` 237 | Type interface{} `json:"type"` 238 | User User `json:"user"` 239 | UserID int64 `json:"user_id"` 240 | BitFlags int `json:"bit_flags"` 241 | ChildCommentCount int `json:"child_comment_count"` 242 | CommentIndex int `json:"comment_index"` 243 | CommentLikeCount int `json:"comment_like_count"` 244 | ContentType string `json:"content_type"` 245 | CreatedAt int64 `json:"created_at"` 246 | CreatedAtUtc int64 `json:"created_at_utc"` 247 | DidReportAsSpam bool `json:"did_report_as_spam"` 248 | HasLikedComment bool `json:"has_liked_comment"` 249 | InlineComposerDisplayCondition string `json:"inline_composer_display_condition"` 250 | OtherPreviewUsers []*User `json:"other_preview_users"` 251 | PreviewChildComments []Comment `json:"preview_child_comments"` 252 | NextMaxChildCursor string `json:"next_max_child_cursor,omitempty"` 253 | HasMoreTailChildComments bool `json:"has_more_tail_child_comments,omitempty"` 254 | NextMinChildCursor string `json:"next_min_child_cursor,omitempty"` 255 | HasMoreHeadChildComments bool `json:"has_more_head_child_comments,omitempty"` 256 | NumTailChildComments int `json:"num_tail_child_comments,omitempty"` 257 | NumHeadChildComments int `json:"num_head_child_comments,omitempty"` 258 | ShareEnabled bool `json:"share_enabled"` 259 | IsCovered bool `json:"is_covered"` 260 | PrivateReplyStatus int64 `json:"private_reply_status"` 261 | SupporterInfo struct { 262 | SupportTier string `json:"support_tier"` 263 | BadgesCount int `json:"badges_count"` 264 | } `json:"supporter_info"` 265 | Status string `json:"status"` 266 | } 267 | 268 | func (c *Comment) setValues(insta *Instagram) { 269 | c.User.insta = insta 270 | for i := range c.OtherPreviewUsers { 271 | c.OtherPreviewUsers[i].insta = insta 272 | } 273 | for i := range c.PreviewChildComments { 274 | c.PreviewChildComments[i].setValues(insta) 275 | } 276 | } 277 | 278 | func (c Comment) getid() string { 279 | return toString(c.ID) 280 | } 281 | 282 | // Like likes comment. 283 | func (c *Comment) Like() error { 284 | return c.changeLike(urlCommentLike) 285 | } 286 | 287 | // Unlike unlikes comment. 288 | func (c *Comment) Unlike() error { 289 | return c.changeLike(urlCommentUnlike) 290 | } 291 | 292 | func (c *Comment) changeLike(endpoint string) error { 293 | insta := c.insta 294 | item := c.item 295 | query := map[string]string{ 296 | "feed_position": "0", 297 | "container_module": "feed_timeline", 298 | "nav_chain": "", 299 | "_uid": toString(insta.Account.ID), 300 | "_uuid": insta.uuid, 301 | "radio_type": "wifi-none", 302 | "is_carousel_bumped_post": "false", // not sure when this would be true 303 | } 304 | if item.IsCommercial { 305 | query["delivery_class"] = "ad" 306 | } else { 307 | query["delivery_class"] = "organic" 308 | } 309 | if item.InventorySource != "" { 310 | query["inventory_source"] = item.InventorySource 311 | } 312 | if len(item.CarouselMedia) > 0 || item.CarouselParentID != "" { 313 | query["carousel_index"] = "0" 314 | } 315 | data, err := json.Marshal(query) 316 | if err != nil { 317 | return err 318 | } 319 | 320 | _, _, err = c.insta.sendRequest( 321 | &reqOptions{ 322 | Endpoint: fmt.Sprintf(endpoint, c.getid()), 323 | IsPost: true, 324 | Query: generateSignature(data), 325 | }, 326 | ) 327 | return err 328 | } 329 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import "errors" 4 | 5 | const ( 6 | // urls 7 | baseUrl = "https://i.instagram.com/" 8 | instaAPIUrl = "https://i.instagram.com/api/v1/" 9 | instaAPIUrlb = "https://b.i.instagram.com/api/v1/" 10 | instaAPIUrlv2 = "https://i.instagram.com/api/v2/" 11 | instaAPIUrlv2b = "https://b.i.instagram.com/api/v2/" 12 | 13 | // header values 14 | bloksVerID = "927f06374b80864ae6a0b04757048065714dc50ff15d2b8b3de8d0b6de961649" 15 | fbAnalytics = "567067343352427" 16 | igCapabilities = "3brTvx0=" 17 | connType = "WIFI" 18 | instaSigKeyVersion = "4" 19 | locale = "en_US" 20 | appVersion = "250.0.0.21.109" 21 | appVersionCode = "394071253" 22 | 23 | // Used for supported_capabilities value used in some requests, e.g. tray requests 24 | supportedSdkVersions = "100.0,101.0,102.0,103.0,104.0,105.0,106.0,107.0,108.0,109.0,110.0,111.0,112.0,113.0,114.0,115.0,116.0,117.0" 25 | facetrackerVersion = "14" 26 | segmentation = "segmentation_enabled" 27 | compression = "ETC2_COMPRESSION" 28 | worldTracker = "world_tracker_enabled" 29 | gyroscope = "gyroscope_enabled" 30 | 31 | // Other 32 | software = "Android RP1A.200720.012.G975FXXSBFUF3" 33 | hmacKey = "iN4$aGr0m" 34 | ) 35 | 36 | var ( 37 | defaultHeaderOptions = map[string]string{ 38 | "X-Ig-Www-Claim": "0", 39 | } 40 | omitAPIHeadersExclude = []string{ 41 | "X-Ig-Bandwidth-Speed-Kbps", 42 | "Ig-U-Shbts", 43 | "X-Ig-Mapped-Locale", 44 | "X-Ig-Family-Device-Id", 45 | "X-Ig-Android-Id", 46 | "X-Ig-Timezone-Offset", 47 | "X-Ig-Device-Locale", 48 | "X-Ig-Device-Id", 49 | "Ig-Intended-User-Id", 50 | "X-Ig-App-Locale", 51 | "X-Bloks-Is-Layout-Rtl", 52 | "X-Pigeon-Rawclienttime", 53 | "X-Bloks-Version-Id", 54 | "X-Ig-Bandwidth-Totalbytes-B", 55 | "X-Ig-Bandwidth-Totaltime-Ms", 56 | "X-Ig-App-Startup-Country", 57 | "X-Ig-Www-Claim", 58 | "X-Bloks-Is-Panorama-Enabled", 59 | } 60 | // Default Device 61 | GalaxyS10 = Device{ 62 | Manufacturer: "samsung", 63 | Model: "SM-G975F", 64 | CodeName: "beyond2", 65 | AndroidVersion: 30, 66 | AndroidRelease: 11, 67 | ScreenDpi: "560dpi", 68 | ScreenResolution: "1440x2898", 69 | Chipset: "exynos9820", 70 | } 71 | G6 = Device{ 72 | Manufacturer: "LGE/lge", 73 | Model: "LG-H870DS", 74 | CodeName: "lucye", 75 | AndroidVersion: 28, 76 | AndroidRelease: 9, 77 | ScreenDpi: "560dpi", 78 | ScreenResolution: "1440x2698", 79 | Chipset: "lucye", 80 | } 81 | timeOffset = getTimeOffset() 82 | ) 83 | 84 | type muteOption string 85 | 86 | const ( 87 | MuteAll muteOption = "all" 88 | MuteStory muteOption = "reel" 89 | MutePosts muteOption = "post" 90 | ) 91 | 92 | // Endpoints (with format vars) 93 | const ( 94 | // Login 95 | urlMsisdnHeader = "accounts/read_msisdn_header/" 96 | urlGetPrefill = "accounts/get_prefill_candidates/" 97 | urlContactPrefill = "accounts/contact_point_prefill/" 98 | urlGetAccFamily = "multiple_accounts/get_account_family/" 99 | urlZrToken = "zr/token/result/" 100 | urlLogin = "accounts/login/" 101 | urlLogout = "accounts/logout/" 102 | urlAutoComplete = "friendships/autocomplete_user_list/" 103 | urlQeSync = "qe/sync/" 104 | urlSync = "launcher/sync/" 105 | urlLogAttribution = "attribution/log_attribution/" 106 | urlMegaphoneLog = "megaphone/log/" 107 | urlExpose = "qe/expose/" 108 | urlGetNdxSteps = "devices/ndx/api/async_get_ndx_ig_steps/" 109 | urlBanyan = "banyan/banyan/" 110 | urlCooldowns = "qp/get_cooldowns/" 111 | urlFetchConfig = "loom/fetch_config/" 112 | urlBootstrapUserScores = "scores/bootstrap/users/" 113 | urlStoreClientPushPermissions = "notifications/store_client_push_permissions/" 114 | urlProcessContactPointSignals = "accounts/process_contact_point_signals/" 115 | 116 | // Account 117 | urlCurrentUser = "accounts/current_user/" 118 | urlChangePass = "accounts/change_password/" 119 | urlSetPrivate = "accounts/set_private/" 120 | urlSetPublic = "accounts/set_public/" 121 | urlRemoveProfPic = "accounts/remove_profile_picture/" 122 | urlChangeProfPic = "accounts/change_profile_picture/" 123 | urlFeedSaved = "feed/saved/all/" 124 | urlFeedSavedPosts = "feed/saved/posts/" 125 | urlFeedSavedIGTV = "feed/saved/igtv/" 126 | urlEditProfile = "accounts/edit_profile/" 127 | urlFeedLiked = "feed/liked/" 128 | urlConsent = "consent/existing_user_flow/" 129 | urlNotifBadge = "notifications/badge/" 130 | urlFeaturedAccounts = "multiple_accounts/get_featured_accounts/" 131 | 132 | // Collections 133 | urlCollectionsList = "collections/list/" 134 | urlCollectionsCreate = "collections/create/" 135 | urlCollectionEdit = "collections/%s/edit/" 136 | urlCollectionDelete = "collections/%s/delete/" 137 | urlCollectionFeedAll = "feed/collection/%s/all/" 138 | urlCollectionFeedPosts = "feed/collection/%s/posts/" 139 | 140 | // Account and profile 141 | urlFollowers = "friendships/%d/followers/" 142 | urlFollowing = "friendships/%d/following/" 143 | 144 | // Users 145 | urlUserArchived = "feed/only_me_feed/" 146 | urlUserByName = "users/%s/usernameinfo/" 147 | urlUserByID = "users/%s/info/" 148 | urlUserBlock = "friendships/block/%d/" 149 | urlUserUnblock = "friendships/unblock/%d/" 150 | urlUserMute = "friendships/mute_posts_or_story_from_follow/" 151 | urlUserUnmute = "friendships/unmute_posts_or_story_from_follow/" 152 | urlUserFollow = "friendships/create/%d/" 153 | urlUserUnfollow = "friendships/destroy/%d/" 154 | urlUserFeed = "feed/user/%d/" 155 | urlFriendship = "friendships/show/%d/" 156 | urlFriendshipShowMany = "friendships/show_many/" 157 | urlFriendshipPending = "friendships/pending/" 158 | urlFriendshipPendingCount = "friendships/pending_follow_requests_count/" 159 | urlFriendshipApprove = "friendships/approve/%d/" 160 | urlFriendshipIgnore = "friendships/ignore/%d/" 161 | urlUserStories = "feed/user/%d/story/" 162 | urlUserTags = "usertags/%d/feed/" 163 | urlBlockedList = "users/blocked_list/" 164 | urlUserInfo = "users/%d/info/" 165 | urlUserHighlights = "highlights/%d/highlights_tray/" 166 | 167 | // Timeline 168 | urlTimeline = "feed/timeline/" 169 | urlStories = "feed/reels_tray/" 170 | urlReelMedia = "feed/reels_media/" 171 | 172 | // Search 173 | urlSearchTop = "fbsearch/topsearch_flat/" 174 | urlSearchUser = "users/search/" 175 | urlSearchTag = "tags/search/" 176 | urlSearchLocation = "fbsearch/places/" 177 | urlSearchRecent = "fbsearch/recent_searches/" 178 | urlSearchNullState = "fbsearch/nullstate_dynamic_sections/" 179 | urlSearchRegisterClick = "fbsearch/register_recent_search_click/" 180 | 181 | // Feeds 182 | urlFeedLocationID = "feed/location/%d/" 183 | urlFeedLocations = "locations/%d/sections/" 184 | urlFeedTag = "feed/tag/%s/" 185 | urlFeedNewPostsExist = "feed/new_feed_posts_exist/" 186 | 187 | // Media 188 | urlMediaInfo = "media/%s/info/" 189 | urlMediaDelete = "media/%s/delete/" 190 | urlMediaLike = "media/%s/like/" 191 | urlMediaUnlike = "media/%s/unlike/" 192 | urlMediaSave = "media/%s/save/" 193 | urlMediaUnsave = "media/%s/unsave/" 194 | urlMediaSeen = "media/seen/" 195 | urlMediaLikers = "media/%s/likers/" 196 | urlMediaBlocked = "media/blocked/" 197 | urlMediaCommentInfos = "media/comment_infos/" 198 | 199 | // Broadcasts 200 | urlLiveInfo = "live/%d/info/" 201 | urlLiveComments = "live/%d/get_comment/" 202 | urlLiveLikeCount = "live/%d/get_like_count/" 203 | urlLiveHeartbeat = "live/%d/heartbeat_and_get_viewer_count/" 204 | urlLiveChaining = "live/get_live_chaining/" 205 | 206 | // IGTV 207 | urlIGTVDiscover = "igtv/discover/" 208 | urlIGTVChannel = "igtv/channel/" 209 | urlIGTVSeries = "igtv/series/all_user_series/%d/" 210 | urlIGTVSeen = "igtv/write_seen_state/" 211 | 212 | // Discover 213 | urlDiscoverExplore = "discover/topical_explore/" 214 | 215 | // Comments 216 | urlCommentAdd = "media/%d/comment/" 217 | urlCommentDelete = "media/%s/comment/%s/delete/" 218 | urlCommentBulkDelete = "media/%s/comment/bulk_delete/" 219 | urlCommentSync = "media/%s/comments/" 220 | urlCommentDisable = "media/%s/disable_comments/" 221 | urlCommentEnable = "media/%s/enable_comments/" 222 | urlCommentLike = "media/%s/comment_like/" 223 | urlCommentUnlike = "media/%s/comment_unlike/" 224 | urlCommentOffensive = "media/comment/check_offensive_comment/" 225 | 226 | // Activity 227 | urlActivityFollowing = "news/" 228 | urlActivityRecent = "news/inbox/" 229 | urlActivitySeen = "news/inbox_seen/" 230 | 231 | // Inbox 232 | urlInbox = "direct_v2/inbox/" 233 | urlInboxPending = "direct_v2/pending_inbox/" 234 | urlInboxSend = "direct_v2/threads/broadcast/text/" 235 | urlInboxSendLike = "direct_v2/threads/broadcast/like/" 236 | urlReplyStory = "direct_v2/threads/broadcast/reel_share/" 237 | urlGetByParticipants = "direct_v2/threads/get_by_participants/" 238 | urlInboxThread = "direct_v2/threads/%s/" 239 | urlInboxMute = "direct_v2/threads/%s/mute/" 240 | urlInboxUnmute = "direct_v2/threads/%s/unmute/" 241 | urlInboxGetItems = "direct_v2/threads/%s/get_items/" 242 | urlInboxMsgSeen = "direct_v2/threads/%s/items/%s/seen/" 243 | urlInboxApprove = "direct_v2/threads/%s/approve/" 244 | urlInboxHide = "direct_v2/threads/%s/hide/" 245 | 246 | // Tags 247 | urlTagInfo = "tags/%s/info/" 248 | urlTagStories = "tags/%s/story/" 249 | urlTagContent = "tags/%s/sections/" 250 | 251 | // Upload 252 | urlUploadPhoto = "rupload_igphoto/%s" 253 | urlUploadVideo = "rupload_igvideo/%s" 254 | urlUploadFinishVid = "media/upload_finish/?video=1" 255 | urlConfigure = "media/configure/" 256 | urlConfigureClip = "media/configure_to_clips/" 257 | urlConfigureSidecar = "media/configure_sidecar/" 258 | urlConfigureIGTV = "media/configure_to_igtv/?video=1" 259 | urlConfigureStory = "media/configure_to_story/" 260 | 261 | // 2FA 262 | url2FACheckTrusted = "two_factor/check_trusted_notification_status/" 263 | url2FALogin = "accounts/two_factor_login/" 264 | ) 265 | 266 | // Errors 267 | var ( 268 | RespErr2FA = "two_factor_required" 269 | 270 | // Account & Login Errors 271 | ErrBadPassword = errors.New("password is incorrect") 272 | ErrTooManyRequests = errors.New("too many requests, please wait a few minutes before you try again") 273 | ErrLoggedOut = errors.New("you have been logged out, please log back in") 274 | ErrLoginRequired = errors.New("you are not logged in, please login") 275 | ErrSessionNotSet = errors.New("session identifier is not set, please log in again to set it") 276 | ErrLogoutFailed = errors.New("failed to logout") 277 | 278 | ErrChallengeRequired = errors.New("challenge required") 279 | ErrCheckpointRequired = errors.New("checkpoint required") 280 | ErrCheckpointPassed = errors.New("a checkpoint was thrown, but goinsta managed to solve it. Please call the function again") 281 | ErrChallengeFailed = errors.New("failed to solve challenge automatically") 282 | 283 | Err2FARequired = errors.New("two Factor Autentication required. Please call insta.TwoFactorInfo.Login2FA(code)") 284 | Err2FANoCode = errors.New("2FA seed is not set, and no code was provided. Please do atleast one of them") 285 | ErrInvalidCode = errors.New("the security code provided is incorrect") 286 | 287 | // Upload Errors 288 | ErrInvalidFormat = errors.New("invalid file type, please use one of jpeg, jpg, mp4") 289 | ErrInvalidImage = errors.New("invalid file type, please use a jpeg or jpg image") 290 | ErrCarouselType = ErrInvalidImage 291 | ErrCarouselMediaLimit = errors.New("carousel media limit of 10 exceeded") 292 | ErrStoryBadMediaType = errors.New("when uploading multiple items to your story at once, all have to be mp4") 293 | ErrStoryMediaTooLong = errors.New("story media must not exceed 15 seconds per item") 294 | 295 | // Search Errors 296 | ErrSearchUserNotFound = errors.New("User not found in search result") 297 | 298 | // IGTV 299 | ErrIGTVNoSeries = errors.New( 300 | "User has no IGTV series, unable to fetch. If you think this was a mistake please update the user", 301 | ) 302 | 303 | // Feed Errors 304 | ErrInvalidTab = errors.New("invalid tab, please select top or recent") 305 | ErrNoMore = errors.New("no more posts availible, page end has been reached") 306 | ErrNotHighlight = errors.New("unable to sync, Reel is not of type highlight") 307 | ErrMediaDeleted = errors.New("sorry, this media has been deleted") 308 | 309 | // Inbox 310 | ErrConvNotPending = errors.New("unable to perform action, conversation is not pending") 311 | 312 | // Misc 313 | ErrByteIndexNotFound = errors.New("failed to index byte slice, delim not found") 314 | ErrNoMedia = errors.New("failed to download, no media found") 315 | ErrInstaNotDefined = errors.New( 316 | "insta has not been defined, this is most likely a bug in the code. Please backtrack which call this error came from, and open an issue detailing exactly how you got to this error", 317 | ) 318 | ErrNoValidLogin = errors.New("no valid login found") 319 | ErrNoProfilePicURL = errors.New("no profile picture url was found. Please fetch the profile first") 320 | 321 | // Users 322 | ErrNoPendingFriendship = errors.New("unable to approve or ignore friendship for user, as there is no pending friendship request") 323 | 324 | // Headless 325 | ErrChromeNotFound = errors.New("to solve challenges a (headless) Chrome browser is used, but none was found. Please install Chromium or Google Chrome, and try again") 326 | ) 327 | -------------------------------------------------------------------------------- /contacts.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Contacts struct { 8 | insta *Instagram 9 | } 10 | 11 | type Contact struct { 12 | Numbers []string `json:"phone_numbers"` 13 | Emails []string `json:"email_addresses"` 14 | Name string `json:"first_name"` 15 | } 16 | 17 | type SyncAnswer struct { 18 | Users []struct { 19 | Pk int64 `json:"pk"` 20 | Username string `json:"username"` 21 | FullName string `json:"full_name"` 22 | IsPrivate bool `json:"is_private"` 23 | ProfilePicURL string `json:"profile_pic_url"` 24 | ProfilePicID string `json:"profile_pic_id"` 25 | IsVerified bool `json:"is_verified"` 26 | HasAnonymousProfilePicture bool `json:"has_anonymous_profile_picture"` 27 | ReelAutoArchive string `json:"reel_auto_archive"` 28 | AddressbookName string `json:"addressbook_name"` 29 | } `json:"users"` 30 | Warning string `json:"warning"` 31 | Status string `json:"status"` 32 | } 33 | 34 | func newContacts(insta *Instagram) *Contacts { 35 | return &Contacts{insta: insta} 36 | } 37 | 38 | func (c *Contacts) SyncContacts(contacts *[]Contact) (*SyncAnswer, error) { 39 | byteContacts, err := json.Marshal(contacts) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | syncContacts := &reqOptions{ 45 | Endpoint: `address_book/link/`, 46 | IsPost: true, 47 | Gzip: true, 48 | Query: map[string]string{ 49 | "phone_id": c.insta.pid, 50 | "module": "find_friends_contacts", 51 | "source": "user_setting", 52 | "device_id": c.insta.uuid, 53 | "_uuid": c.insta.uuid, 54 | "contacts": string(byteContacts), 55 | }, 56 | } 57 | 58 | body, _, err := c.insta.sendRequest(syncContacts) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | answ := &SyncAnswer{} 64 | if err := json.Unmarshal(body, answ); err != nil { 65 | return nil, err 66 | } 67 | return answ, nil 68 | } 69 | 70 | func (c *Contacts) UnlinkContacts() error { 71 | unlinkBody := &reqOptions{ 72 | Endpoint: "address_book/unlink/", 73 | IsPost: true, 74 | Query: map[string]string{ 75 | "phone_id": c.insta.pid, 76 | "device_id": c.insta.uuid, 77 | "_uuid": c.insta.uuid, 78 | "user_initiated": "true", 79 | }, 80 | } 81 | 82 | _, _, err := c.insta.sendRequest(unlinkBody) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davincible/goinsta/0fc4930a1e6c8b62fe334e7a55106ddad9b7b4b1/docs/.gitkeep -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | #### Golang + Instagram Private API 2 |

3 | 4 | > Unofficial Instagram API for Golang 5 | 6 | [![Build Status](https://travis-ci.org/Davincible/goinsta.svg?branch=master)](https://travis-ci.org/Davincible/goinsta) [![GoDoc](https://godoc.org/github.com/Davincible/goinsta?status.svg)](https://godoc.org/github.com/Davincible/goinsta) [![Go Report Card](https://goreportcard.com/badge/github.com/Davincible/goinsta)](https://goreportcard.com/report/github.com/Davincible/goinsta) [![Gitter chat](https://badges.gitter.im/goinsta/community.png)](https://gitter.im/goinsta/community) 7 | 8 | ### Features 9 | 10 | * **HTTP2 by default. Goinsta uses HTTP2 client enhancing performance.** 11 | * **Object independency. Can handle multiple instagram accounts.** 12 | * **Like Instagram mobile application**. Goinsta is very similar to Instagram official application. 13 | * **Simple**. Goinsta is made by lazy programmers! 14 | * **Backup methods**. You can use `Export` and `Import` functions. 15 | * **Security**. Your password is only required to login. After login your password is deleted. 16 | * **No External Dependencies**. GoInsta will not use any Go packages outside of the standard library. 17 | 18 | ### Package installation 19 | 20 | `go get -u -v gopkg.in/Davincible/goinsta.v2` 21 | 22 | ### Example 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | 30 | "gopkg.in/Davincible/goinsta.v2" 31 | ) 32 | 33 | func main() { 34 | insta := goinsta.New("USERNAME", "PASSWORD") 35 | 36 | // Export your configuration 37 | // after exporting you can use Import function instead of New function. 38 | // insta, err := goinsta.Import("~/.goinsta") 39 | // it's useful when you want use goinsta repeatedly. 40 | insta.Export("~/.goinsta") 41 | 42 | ... 43 | } 44 | ``` 45 | 46 | ### Projects using `goinsta` 47 | 48 | - [go-instabot](https://github.com/tducasse/go-instabot) 49 | - [nick_bot](https://github.com/icholy/nick_bot) 50 | - [instagraph](https://github.com/Davincible/instagraph) 51 | - [icrawler](https://github.com/themester/icrawler) 52 | - [ermes](https://github.com/borteo/ermes) 53 | - [instafeed](https://github.com/falzm/instafeed) 54 | - [goinstadownload](https://github.com/alejoloaiza/goinstadownload) 55 | - [InstagramStoriesDownloader](https://github.com/DiSiqueira/InstagramStoriesDownloader) 56 | - [gridcube-challenge](https://github.com/rodrwan/gridcube-challenge) 57 | - [nyaakitties](https://github.com/gracechang/nyaakitties) 58 | - [InstaFollower](https://github.com/Unanoc/InstaFollower) 59 | - [follow-sync](https://github.com/kirsle/follow-sync) 60 | - [Game DB](https://github.com/gamedb/gamedb) 61 | - ... 62 | 63 | ### Legal 64 | 65 | This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by Instagram or any of its affiliates or subsidiaries. This is an independent and unofficial API. Use at your own risk. 66 | 67 | ### Versioning 68 | 69 | Goinsta used gopkg.in as versioning control. Stable new API is the version v2.0. You can get it using: 70 | 71 | ```bash 72 | $ go get -u -v gopkg.in/Davincible/goinsta.v2 73 | ``` 74 | 75 | Or 76 | 77 | If you have `GO111MODULE=on` 78 | 79 | ``` 80 | $ go get -u github.com/Davincible/goinsta 81 | ``` 82 | 83 | ### Donate 84 | 85 | **Davincible** 86 | 87 | ![btc](https://raw.githubusercontent.com/reek/anti-adblock-killer/gh-pages/images/bitcoin.png) Bitcoin: `1KjcfrBPJtM4MfBSGTqpC6RcoEW1KBh15X` 88 | 89 | **Mester** 90 | 91 | ![btc](https://raw.githubusercontent.com/reek/anti-adblock-killer/gh-pages/images/bitcoin.png) Bitcoin: `37aogDJYBFkdSJTWG7TgcpgNweGHPCy1Ks` 92 | 93 | 94 | [![Analytics](https://ga-beacon.appspot.com/UA-107698067-1/readme-page)](https://github.com/igrigorik/ga-beacon) 95 | 96 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // EnvPlainAcc represents the plain account details stored in the env variable: 14 | // 15 | // INSTAGRAM_ACT_="username:password" 16 | type EnvPlainAcc struct { 17 | Name string 18 | Username string 19 | Password string 20 | } 21 | 22 | // EnvEncAcc represents the encoded account details stored in the env variable: 23 | // 24 | // INSTAGRAM_BASE64_="" 25 | type EnvEncAcc struct { 26 | Name string 27 | Username string 28 | Base64 string 29 | } 30 | 31 | // EnvAcc represents the pair of plain and base64 encoded account pairs as 32 | // stored in EnvPlainAcc and EnvEncAcc, with env variables: 33 | // 34 | // INSTAGRAM_ACT_="username:password" 35 | // INSTAGRAM_BASE64_="" 36 | type EnvAcc struct { 37 | Plain *EnvPlainAcc 38 | Enc *EnvEncAcc 39 | } 40 | 41 | var ( 42 | errNoAcc = errors.New("no account found") 43 | ) 44 | 45 | // EnvRandAcc will check the environment variables, and the .env file in 46 | // the current working directory (unless another path has been provided), 47 | // for either a base64 encoded goinsta config, or plain credentials. 48 | // 49 | // To use this function, add one or multiple of the following: 50 | // INSTAGRAM_ACT_="username:password" 51 | // INSTAGRAM_BASE64_="" 52 | // 53 | // INSTAGRAM_ACT_ variables will automatiaclly be converted to INSTAGRAM_BASE64_ 54 | // 55 | func EnvRandAcc(path ...string) (*Instagram, error) { 56 | err := checkEnv(path...) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return getRandAcc(path...) 61 | } 62 | 63 | // EnvRandLogin fetches a random login from the env. 64 | // :param: path (OPTIONAL) - path to a file, by default .env 65 | // 66 | // Looks for INSTAGRAM_ACT_="username:password" in env 67 | // 68 | func EnvRandLogin(path ...string) (string, string, error) { 69 | allAccs, err := EnvReadAccs(path...) 70 | if err != nil { 71 | return "", "", err 72 | } 73 | 74 | // extract plain accounts from all accounts 75 | accs := []*EnvPlainAcc{} 76 | for _, acc := range allAccs { 77 | if acc.Plain != nil { 78 | accs = append(accs, acc.Plain) 79 | } 80 | } 81 | 82 | // find valid one, until list exhausted 83 | for { 84 | i := rand.Intn(len(accs)) 85 | r := accs[i] 86 | if r.Username != "" && r.Password != "" { 87 | return r.Username, r.Password, nil 88 | } 89 | accs = append(accs[:i], accs[i+1:]...) 90 | if len(accs) == 0 { 91 | return "", "", ErrNoValidLogin 92 | } 93 | } 94 | } 95 | 96 | // EnvProvision will check the environment variables for INSTAGRAM_ACT_ and 97 | // create a base64 encoded config for the account, and write it to path. 98 | // 99 | // :param: path - path a file to use as env, commonly a .env, but not required. 100 | // :param: refresh (OPTIONAL) - refresh all plaintext credentials, don't skip already converted accounts 101 | // 102 | // This function has been created the use of a .env file in mind. 103 | // 104 | // .env contents: 105 | // INSTAGRAM_ACT_="user:pass" 106 | // 107 | // This function will add to the .env: 108 | // INSTAGRAM_BASE64_="..." 109 | // 110 | func EnvProvision(path string, refresh ...bool) error { 111 | // By default, skip exisitng accounts 112 | refreshFlag := len(refresh) == 0 || (len(refresh) > 0 && !refresh[0]) 113 | fmt.Printf("Force refresh is set to %v\n", refreshFlag) 114 | 115 | accs, _, err := envLoadAccs(path) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | fmt.Printf("Found %d accounts\n", len(accs)) 121 | 122 | for _, acc := range accs { 123 | if acc.Enc != nil && !refreshFlag { 124 | fmt.Printf("Skipping account %s\n", acc.Plain.Name) 125 | continue 126 | } 127 | username := acc.Plain.Username 128 | password := acc.Plain.Password 129 | fmt.Println("Processing", username) 130 | insta := New(username, password) 131 | 132 | if err := insta.Login(); err != nil { 133 | return err 134 | } 135 | 136 | // Export Config 137 | enc, err := insta.ExportAsBase64String() 138 | if err != nil { 139 | return err 140 | } 141 | acc.Enc.Base64 = enc 142 | fmt.Println("Sleeping...") 143 | time.Sleep(20 * time.Second) 144 | } 145 | err = accsToFile(path, accs) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | 153 | // EnvUpdateAccs will update the plain and encoded account variables stored in 154 | // the .env file: 155 | // 156 | // INSTAGRAM_ACT_="username:password" 157 | // INSTAGRAM_BASE64_="" 158 | // 159 | // :param: string:path -- file path of the .env file, typically ".env" 160 | // :param: []*EncAcc:newAccs -- list of updated versions of the accounts 161 | func EnvUpdateAccs(path string, newAccs []*EnvAcc) error { 162 | return envUpdateAccs(path, newAccs) 163 | } 164 | 165 | // EnvUpdateEnc will update the encoded account variables stored in 166 | // the .env file: 167 | // 168 | // INSTAGRAM_BASE64_="" 169 | // 170 | // :param: string:path -- file path of the .env file, typically ".env" 171 | // :param: []*EnvEncAcc:newAccs -- list of updated encoded accounts 172 | func EnvUpdateEnc(path string, newAccs []*EnvEncAcc) error { 173 | return envUpdateAccs(path, newAccs) 174 | } 175 | 176 | // EnvPlainAccs will update the plain account variables stored in 177 | // the .env file: 178 | // 179 | // INSTAGRAM_ACT_="username:password" 180 | // 181 | // :param: string:path -- file path of the .env file, typically ".env" 182 | // :param: []*EnvPlainAcc:newAccs -- list of updated plain accounts 183 | func EnvUpdatePlain(path string, newAccs []*EnvPlainAcc) error { 184 | return envUpdateAccs(path, newAccs) 185 | } 186 | 187 | func envUpdateAccs(path string, newAccs interface{}) error { 188 | accs, _, err := dotenv(path) 189 | if err != nil { 190 | return err 191 | } 192 | switch n := newAccs.(type) { 193 | case []*EnvAcc: 194 | 195 | case []*EnvPlainAcc: 196 | for _, acc := range n { 197 | accs = addOrUpdateAcc(accs, acc) 198 | } 199 | case []*EnvEncAcc: 200 | for _, acc := range n { 201 | accs = addOrUpdateAcc(accs, acc) 202 | } 203 | } 204 | 205 | return accsToFile(path, accs) 206 | } 207 | 208 | // checkEnv will check the env variables for accounts that do have a login, 209 | // but no config (INSTAGRAM_BASE64_<...>). If one is found, call ProvisionEnv 210 | func checkEnv(path ...string) error { 211 | accs, err := EnvReadAccs(path...) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | // Check for every plain acc, if there is an encoded equivalent 217 | for _, acc := range accs { 218 | if acc.Plain != nil && acc.Enc == nil { 219 | fmt.Println("Unable to find:", acc.Plain.Name, acc.Plain.Username) 220 | p := ".env" 221 | if len(path) > 0 { 222 | p = path[0] 223 | } 224 | if err := EnvProvision(p, true); err != nil { 225 | return err 226 | } 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | // getRandAcc returns a random insta instance from env 233 | func getRandAcc(path ...string) (*Instagram, error) { 234 | allAccs, err := EnvReadAccs(path...) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | // extract encoded accounts from all accounts 240 | accounts := []*EnvEncAcc{} 241 | for _, acc := range allAccs { 242 | if acc.Enc != nil { 243 | accounts = append(accounts, acc.Enc) 244 | } 245 | } 246 | 247 | // validate result 248 | if len(accounts) == 0 { 249 | return nil, ErrNoValidLogin 250 | } 251 | 252 | // select rand account 253 | rand.Seed(time.Now().UnixNano()) 254 | r := rand.Intn(len(accounts)) 255 | 256 | // load account config 257 | insta, err := ImportFromBase64String(accounts[r].Base64, true) 258 | if err != nil { 259 | return nil, err 260 | } 261 | // insta.SetProxy("http://localhost:9090", false, true) 262 | return insta, err 263 | } 264 | 265 | // EnvLoadPlain will load all plain accounts stored in the env variables: 266 | // 267 | // INSTAGRAM_ACT_="username:password" 268 | // 269 | // :param: path (OPTIONAL) -- .env file to load, default to ".env" 270 | func EnvLoadPlain(path ...string) ([]*EnvPlainAcc, error) { 271 | allAccs, err := EnvReadAccs(path...) 272 | if err != nil { 273 | return nil, err 274 | } 275 | 276 | // extract plain accounts from all accounts 277 | accs := []*EnvPlainAcc{} 278 | for _, acc := range allAccs { 279 | if acc.Plain != nil { 280 | accs = append(accs, acc.Plain) 281 | } 282 | } 283 | return accs, nil 284 | } 285 | 286 | // EnvLoadAccs loads all the environment variables. 287 | // 288 | // By default, the OS environment variables as well as .env are loaded 289 | // To load a custom file, instead of .env, pass the filepath as an argument. 290 | // 291 | // Don't Sync param is set to true to prevent any http calls on import by default 292 | func EnvLoadAccs(p ...string) ([]*Instagram, error) { 293 | instas := []*Instagram{} 294 | accs, _, err := envLoadAccs(p...) 295 | 296 | for _, acc := range accs { 297 | insta, err := ImportFromBase64String(acc.Enc.Base64, true) 298 | if err != nil { 299 | return nil, err 300 | } 301 | instas = append(instas, insta) 302 | } 303 | 304 | return instas, err 305 | } 306 | 307 | // EnvReadAccs loads both all plain and base64 encoded accounts 308 | // 309 | // Set in a .env file or export to your environment variables: 310 | // INSTAGRAM_ACT_="user:pass" 311 | // INSTAGRAM_BASE64_="..." 312 | // 313 | // :param: p (OPTIONAL) -- env file path, ".env" by default 314 | func EnvReadAccs(p ...string) ([]*EnvAcc, error) { 315 | accs, _, err := envLoadAccs(p...) 316 | return accs, err 317 | } 318 | 319 | func envLoadAccs(p ...string) ([]*EnvAcc, []string, error) { 320 | path := ".env" 321 | if len(p) > 0 { 322 | path = p[0] 323 | } 324 | environ := os.Environ() 325 | accsOne, other, err := dotenv(path) 326 | if err != nil { 327 | return nil, nil, err 328 | } 329 | 330 | accsTwo, _, err := parseAccs(environ) 331 | if err != nil { 332 | return nil, nil, err 333 | } 334 | accs := append(accsOne, accsTwo...) 335 | if len(accs) == 0 { 336 | return nil, nil, ErrNoValidLogin 337 | } 338 | 339 | return accs, other, nil 340 | } 341 | 342 | // dotenv loads the file in the path parameter (commonly a .env file) and 343 | // returns its contents. 344 | func dotenv(path string) ([]*EnvAcc, []string, error) { 345 | file, err := os.Open(path) 346 | if err != nil { 347 | return nil, nil, err 348 | } 349 | defer file.Close() 350 | 351 | buf := new(bytes.Buffer) 352 | _, err = buf.ReadFrom(file) 353 | if err != nil { 354 | return nil, nil, err 355 | } 356 | lines := strings.Split(buf.String(), "\n") 357 | accs, other, err := parseAccs(lines) 358 | if err != nil { 359 | return nil, nil, err 360 | } 361 | return accs, other, nil 362 | } 363 | 364 | func parseAccs(lines []string) ([]*EnvAcc, []string, error) { 365 | accs := []*EnvAcc{} 366 | other := []string{} 367 | for _, line := range lines { 368 | plain, errPlain := parsePlain(line) 369 | if errPlain == nil { 370 | accs = addOrUpdateAcc(accs, plain) 371 | } 372 | 373 | enc, errEnc := parseEnc(line) 374 | if errEnc == nil { 375 | accs = addOrUpdateAcc(accs, enc) 376 | } else if errEnc != errNoAcc { 377 | return nil, nil, errEnc 378 | } 379 | 380 | // Keep track of other lines in the file, not related to goinsta 381 | if !strings.HasPrefix(line, "INSTAGRAM") { 382 | other = append(other, line) 383 | } 384 | } 385 | return accs, other, nil 386 | } 387 | 388 | func parsePlain(line string) (*EnvPlainAcc, error) { 389 | if strings.HasPrefix(line, "INSTAGRAM_ACT_") { 390 | envVar := strings.Split(line, "=") 391 | name := strings.Split(envVar[0], "_")[2] 392 | acc := envVar[1] 393 | if acc[0] == '"' { 394 | acc = acc[1 : len(acc)-1] 395 | } 396 | creds := strings.Split(acc, ":") 397 | 398 | return &EnvPlainAcc{ 399 | Name: name, 400 | Username: strings.ToLower(creds[0]), 401 | Password: creds[1], 402 | }, nil 403 | } 404 | return nil, errNoAcc 405 | } 406 | 407 | func parseEnc(line string) (*EnvEncAcc, error) { 408 | if strings.HasPrefix(line, "INSTAGRAM_BASE64_") { 409 | envVar := strings.SplitN(line, "=", 2) 410 | encodedString := strings.TrimSpace(envVar[1]) 411 | name := strings.Split(envVar[0], "_")[2] 412 | if encodedString[0] == '"' { 413 | encodedString = encodedString[1 : len(encodedString)-1] 414 | } 415 | insta, err := ImportFromBase64String(encodedString, true) 416 | if err != nil { 417 | return nil, err 418 | } 419 | 420 | return &EnvEncAcc{ 421 | Name: name, 422 | Username: insta.Account.Username, 423 | Base64: encodedString, 424 | }, nil 425 | } 426 | return nil, errNoAcc 427 | } 428 | 429 | func addOrUpdateAcc(accs []*EnvAcc, toAdd interface{}) []*EnvAcc { 430 | switch newAcc := toAdd.(type) { 431 | case *EnvAcc: 432 | for _, acc := range accs { 433 | if (acc.Enc != nil && newAcc.Plain.Username == acc.Enc.Username) || 434 | (acc.Plain != nil && newAcc.Enc.Username == acc.Plain.Username) { 435 | if newAcc.Plain != nil { 436 | if newAcc.Plain.Name == "" { 437 | newAcc.Plain.Name = acc.Plain.Name 438 | } 439 | 440 | acc.Plain = newAcc.Plain 441 | } 442 | if newAcc.Enc != nil { 443 | if newAcc.Enc.Name == "" { 444 | newAcc.Enc.Name = acc.Enc.Name 445 | } 446 | acc.Enc = newAcc.Enc 447 | } 448 | return accs 449 | } 450 | } 451 | case *EnvPlainAcc: 452 | for _, acc := range accs { 453 | if (acc.Enc != nil && newAcc.Username == acc.Enc.Username) || 454 | (acc.Plain != nil && newAcc.Username == acc.Plain.Username) { 455 | if newAcc.Name == "" { 456 | newAcc.Name = acc.Plain.Name 457 | } 458 | acc.Plain = newAcc 459 | return accs 460 | } 461 | } 462 | accs = append(accs, &EnvAcc{Plain: newAcc}) 463 | case *EnvEncAcc: 464 | for _, acc := range accs { 465 | if (acc.Plain != nil && newAcc.Username == acc.Plain.Username) || 466 | (acc.Enc != nil && newAcc.Username == acc.Enc.Username) { 467 | if newAcc.Name == "" { 468 | newAcc.Name = acc.Enc.Name 469 | } 470 | acc.Enc = newAcc 471 | return accs 472 | } 473 | } 474 | accs = append(accs, &EnvAcc{Enc: newAcc}) 475 | } 476 | return accs 477 | } 478 | 479 | func accsToFile(path string, accs []*EnvAcc) error { 480 | newBuf := new(bytes.Buffer) 481 | 482 | for _, acc := range accs { 483 | if acc.Plain != nil { 484 | line := fmt.Sprintf("INSTAGRAM_ACT_%s=\"%s:%s\"\n", acc.Plain.Name, acc.Plain.Username, acc.Plain.Password) 485 | _, err := newBuf.WriteString(line) 486 | if err != nil { 487 | return err 488 | } 489 | 490 | } 491 | if acc.Enc != nil { 492 | encLine := fmt.Sprintf("INSTAGRAM_BASE64_%s=\"%s\"\n\n", acc.Enc.Name, acc.Enc.Base64) 493 | _, err := newBuf.WriteString(encLine) 494 | if err != nil { 495 | return err 496 | } 497 | } 498 | } 499 | 500 | if err := os.WriteFile(path, newBuf.Bytes(), 0o644); err != nil { 501 | return err 502 | } 503 | 504 | return nil 505 | } 506 | 507 | func (acc *Account) GetEnvEncAcc() (*EnvEncAcc, error) { 508 | b, err := acc.insta.ExportAsBase64String() 509 | return &EnvEncAcc{ 510 | Username: acc.Username, 511 | Base64: b, 512 | }, err 513 | } 514 | -------------------------------------------------------------------------------- /explore.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import "encoding/json" 4 | 5 | type Discover struct { 6 | insta *Instagram 7 | sessionId string 8 | err error 9 | Items []DiscoverSectionalItem 10 | NumResults int 11 | 12 | AutoLoadMoreEnabled bool `json:"auto_load_more_enabled"` 13 | Clusters []struct { 14 | CanMute bool `json:"can_mute"` 15 | Context string `json:"context"` 16 | DebugInfo string `json:"debug_info"` 17 | Description string `json:"description"` 18 | ID interface{} `json:"id"` 19 | IsMuted bool `json:"is_muted"` 20 | Labels interface{} `json:"labels"` 21 | Name string `json:"name"` 22 | Title string `json:"title"` 23 | Type string `json:"type"` 24 | } `json:"clusters"` 25 | MaxID string `json:"max_id"` 26 | MoreAvailable bool `json:"more_available"` 27 | NextID string `json:"next_max_id"` 28 | RankToken string `json:"rank_token"` 29 | SectionalItems []DiscoverSectionalItem `json:"sectional_items"` 30 | SessionPagingToken string `json:"session_paging_token"` 31 | Status string `json:"status"` 32 | } 33 | 34 | type DiscoverMediaItem struct { 35 | Media Item `json:"media"` 36 | } 37 | 38 | type DiscoverSectionalItem struct { 39 | ExploreItemInfo struct { 40 | AspectRatio float64 `json:"aspect_ratio"` 41 | Autoplay bool `json:"autoplay"` 42 | NumColumns int `json:"num_columns"` 43 | TotalNumColumns int `json:"total_num_columns"` 44 | } `json:"explore_item_info"` 45 | FeedType string `json:"feed_type"` 46 | LayoutContent struct { 47 | // Usually not all of these are filled 48 | // I have often seen the 1 out of 5 items being the ThreeByFour 49 | // and the third item the TwoByTwoItems + Fill Items, 50 | // the others tend to be Medias, but this is not always the case 51 | LayoutType string `json:"layout_type"` 52 | Medias []DiscoverMediaItem `json:"medias"` 53 | 54 | FillItems []DiscoverMediaItem `json:"fill_items"` 55 | OneByOneItem DiscoverMediaItem `json:"one_by_one"` 56 | TwoByTwoItem DiscoverMediaItem `json:"two_by_two_item"` 57 | 58 | ThreeByFourItem struct { 59 | // TODO: this is a reels section, which you can paginate on its own 60 | Clips struct { 61 | ContentSource string `json:"content_source"` 62 | Design string `json:"design"` 63 | ID string `json:"id"` 64 | Items []DiscoverMediaItem `json:"items"` 65 | Label string `json:"label"` 66 | MaxID string `json:"max_id"` 67 | MoreAvailable bool `json:"more_available"` 68 | Type string `json:"type"` 69 | } `json:"clips"` 70 | } `json:"three_by_four_item"` 71 | } `json:"layout_content"` 72 | LayoutType string `json:"layout_type"` 73 | } 74 | 75 | func newDiscover(insta *Instagram) *Discover { 76 | return &Discover{ 77 | insta: insta, 78 | sessionId: generateUUID(), 79 | } 80 | } 81 | 82 | // Next allows you to paginate explore page results. Also use this for your 83 | // first fetch 84 | func (disc *Discover) Next() bool { 85 | if disc.sessionId == "" { 86 | disc.sessionId = generateUUID() 87 | } 88 | 89 | query := map[string]string{ 90 | "omit_cover_media": "true", 91 | "reels_configuration": "default", 92 | "use_sectional_payload": "true", 93 | "timezone_offset": timeOffset, 94 | "session_id": disc.sessionId, 95 | "include_fixed_destinations": "true", 96 | } 97 | 98 | if disc.NextID != "" { 99 | query["max_id"] = disc.NextID 100 | query["is_prefetch"] = "false" 101 | } else { 102 | query["is_prefetch"] = "true" 103 | } 104 | 105 | body, _, err := disc.insta.sendRequest(&reqOptions{ 106 | Endpoint: urlDiscoverExplore, 107 | Query: query, 108 | }) 109 | if err != nil { 110 | disc.err = err 111 | return false 112 | } 113 | 114 | err = json.Unmarshal(body, disc) 115 | if err != nil { 116 | disc.err = err 117 | return false 118 | } 119 | disc.setValues() 120 | disc.Items = append(disc.Items, disc.SectionalItems...) 121 | disc.NumResults = len(disc.SectionalItems) 122 | return true 123 | } 124 | 125 | // Error will return the error, if one is present 126 | func (disc *Discover) Error() error { 127 | return disc.err 128 | } 129 | 130 | // Refresh will remove the session token, and frefresh the results, like a pull down 131 | func (disc *Discover) Refresh() bool { 132 | disc.sessionId = generateUUID() 133 | disc.NextID = "" 134 | return disc.Next() 135 | } 136 | 137 | func (disc *Discover) setValues() { 138 | for _, sec := range disc.SectionalItems { 139 | for _, i := range sec.LayoutContent.Medias { 140 | i.Media.insta = disc.insta 141 | i.Media.User.insta = disc.insta 142 | } 143 | for _, i := range sec.LayoutContent.FillItems { 144 | i.Media.insta = disc.insta 145 | i.Media.User.insta = disc.insta 146 | } 147 | for _, i := range sec.LayoutContent.ThreeByFourItem.Clips.Items { 148 | i.Media.insta = disc.insta 149 | i.Media.User.insta = disc.insta 150 | } 151 | sec.LayoutContent.OneByOneItem.Media.insta = disc.insta 152 | sec.LayoutContent.OneByOneItem.Media.User.insta = disc.insta 153 | sec.LayoutContent.TwoByTwoItem.Media.insta = disc.insta 154 | sec.LayoutContent.TwoByTwoItem.Media.User.insta = disc.insta 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /feeds.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Feed is the object for all feed endpoints. 9 | type Feed struct { 10 | insta *Instagram 11 | } 12 | 13 | // newFeed creates new Feed structure 14 | func newFeed(insta *Instagram) *Feed { 15 | return &Feed{ 16 | insta: insta, 17 | } 18 | } 19 | 20 | // Feed search by locationID 21 | func (feed *Feed) LocationID(locationID int64) (*FeedLocation, error) { 22 | insta := feed.insta 23 | body, _, err := insta.sendRequest( 24 | &reqOptions{ 25 | Endpoint: fmt.Sprintf(urlFeedLocationID, locationID), 26 | Query: map[string]string{ 27 | "rank_token": insta.rankToken, 28 | "ranked_content": "true", 29 | }, 30 | }, 31 | ) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | res := &FeedLocation{} 37 | err = json.Unmarshal(body, res) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | for _, i := range res.RankedItems { 43 | i.insta = insta 44 | } 45 | for _, i := range res.Items { 46 | i.insta = insta 47 | } 48 | return res, nil 49 | } 50 | 51 | // FeedLocation is the struct that fits the structure returned by instagram on LocationID search. 52 | type FeedLocation struct { 53 | RankedItems []*Item `json:"ranked_items"` 54 | Items []*Item `json:"items"` 55 | NumResults int `json:"num_results"` 56 | NextID string `json:"next_max_id"` 57 | MoreAvailable bool `json:"more_available"` 58 | AutoLoadMoreEnabled bool `json:"auto_load_more_enabled"` 59 | MediaCount int `json:"media_count"` 60 | Location Location `json:"location"` 61 | Status string `json:"status"` 62 | } 63 | 64 | // Tags search by Tag in user Feed 65 | // 66 | // This method does not perform a search for a tag, but directly queries the 67 | // feed items for the specified Tag. The preffered way would be to search 68 | // for the tag, call TopSearchItem.RegisterClick(), and then fetch the feed. 69 | // 70 | // This method uses an older endpoint, although it still seems to work. 71 | // The preffered way to fetch Hashtag feeds is by using the Hashtag struct. 72 | // This can be obtained from insta.NewHashtag(tag), or insta.Searchbar.SearchHashtag(tag) 73 | // 74 | func (feed *Feed) Tags(tag string) (*FeedTag, error) { 75 | insta := feed.insta 76 | body, _, err := insta.sendRequest( 77 | &reqOptions{ 78 | Endpoint: fmt.Sprintf(urlFeedTag, tag), 79 | Query: map[string]string{ 80 | "rank_token": insta.rankToken, 81 | "ranked_content": "true", 82 | }, 83 | }, 84 | ) 85 | if err != nil { 86 | return nil, err 87 | } 88 | res := &FeedTag{ 89 | insta: insta, 90 | } 91 | err = json.Unmarshal(body, res) 92 | if err != nil { 93 | return nil, err 94 | } 95 | res.name = tag 96 | res.setValues() 97 | 98 | return res, nil 99 | } 100 | 101 | // FeedTag is the struct that fits the structure returned by instagram on TagSearch. 102 | type FeedTag struct { 103 | insta *Instagram 104 | err error 105 | 106 | name string 107 | 108 | RankedItems []*Item `json:"ranked_items"` 109 | Items []*Item `json:"items"` 110 | NumResults int `json:"num_results"` 111 | NextID string `json:"next_max_id"` 112 | MoreAvailable bool `json:"more_available"` 113 | AutoLoadMoreEnabled bool `json:"auto_load_more_enabled"` 114 | Story StoryMedia `json:"story"` 115 | Status string `json:"status"` 116 | } 117 | 118 | func (ft *FeedTag) setValues() { 119 | for i := range ft.RankedItems { 120 | ft.RankedItems[i].insta = ft.insta 121 | ft.RankedItems[i].media = &FeedMedia{ 122 | insta: ft.insta, 123 | NextID: ft.RankedItems[i].ID, 124 | } 125 | } 126 | 127 | for i := range ft.Items { 128 | ft.Items[i].insta = ft.insta 129 | ft.Items[i].media = &FeedMedia{ 130 | insta: ft.insta, 131 | NextID: ft.Items[i].ID, 132 | } 133 | } 134 | } 135 | 136 | // Next paginates over hashtag feed. 137 | func (ft *FeedTag) Next() bool { 138 | if ft.err != nil { 139 | return false 140 | } 141 | 142 | insta := ft.insta 143 | name := ft.name 144 | body, _, err := insta.sendRequest( 145 | &reqOptions{ 146 | Query: map[string]string{ 147 | "max_id": ft.NextID, 148 | "rank_token": insta.rankToken, 149 | }, 150 | Endpoint: fmt.Sprintf(urlFeedTag, name), 151 | }, 152 | ) 153 | if err == nil { 154 | newFT := &FeedTag{ 155 | insta: insta, 156 | } 157 | err = json.Unmarshal(body, newFT) 158 | if err == nil { 159 | *ft = *newFT 160 | ft.insta = insta 161 | ft.name = name 162 | if !ft.MoreAvailable { 163 | ft.err = ErrNoMore 164 | } 165 | ft.setValues() 166 | return true 167 | } 168 | } 169 | ft.err = err 170 | return false 171 | } 172 | 173 | // Error returns hashtag error 174 | func (ft *FeedTag) Error() error { 175 | return ft.err 176 | } 177 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "fmt" 11 | "io" 12 | "time" 13 | ) 14 | 15 | const ( 16 | volatileSeed = "12345" 17 | ) 18 | 19 | func generateMD5Hash(text string) string { 20 | hasher := md5.New() 21 | hasher.Write([]byte(text)) 22 | return hex.EncodeToString(hasher.Sum(nil)) 23 | } 24 | 25 | func generateHMAC(text, key string) string { 26 | hasher := hmac.New(sha256.New, []byte(key)) 27 | hasher.Write([]byte(text)) 28 | return hex.EncodeToString(hasher.Sum(nil)) 29 | } 30 | 31 | func generateDeviceID(seed string) string { 32 | hash := generateMD5Hash(seed + volatileSeed) 33 | return "android-" + hash[:16] 34 | } 35 | 36 | func generateUserBreadcrumb(text string) string { 37 | ts := time.Now().Unix() 38 | d := fmt.Sprintf("%d %d %d %d%d", 39 | len(text), 0, random(3000, 10000), ts, random(100, 999)) 40 | hmac := base64.StdEncoding.EncodeToString([]byte(generateHMAC(d, hmacKey))) 41 | enc := base64.StdEncoding.EncodeToString([]byte(d)) 42 | return hmac + "\n" + enc + "\n" 43 | } 44 | 45 | // generateSignature takes a string of byte slice as argument, and prepents the signature 46 | func generateSignature(d interface{}, extra ...map[string]string) map[string]string { 47 | var data string 48 | switch x := d.(type) { 49 | case []byte: 50 | data = string(x) 51 | case string: 52 | data = x 53 | } 54 | r := map[string]string{ 55 | "signed_body": "SIGNATURE." + data, 56 | } 57 | for _, e := range extra { 58 | for k, v := range e { 59 | r[k] = v 60 | } 61 | } 62 | 63 | return r 64 | } 65 | 66 | func newUUID() (string, error) { 67 | uuid := make([]byte, 16) 68 | n, err := io.ReadFull(rand.Reader, uuid) 69 | if n != len(uuid) || err != nil { 70 | return "", err 71 | } 72 | // variant bits; see section 4.1.1 73 | uuid[8] = uuid[8]&^0xc0 | 0x80 74 | // version 4 (pseudo-random); see section 4.1.3 75 | uuid[6] = uuid[6]&^0xf0 | 0x40 76 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil 77 | } 78 | 79 | func generateUUID() string { 80 | uuid, err := newUUID() 81 | if err != nil { 82 | return "cb479ee7-a50d-49e7-8b7b-60cc1a105e22" // default value when error occurred 83 | } 84 | return uuid 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Davincible/goinsta/v3 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/chromedp/cdproto v0.0.0-20220901095120-1a01299a2163 7 | github.com/chromedp/chromedp v0.8.5 8 | ) 9 | 10 | require ( 11 | github.com/chromedp/sysutil v1.0.0 // indirect 12 | github.com/gobwas/httphead v0.1.0 // indirect 13 | github.com/gobwas/pool v0.2.1 // indirect 14 | github.com/gobwas/ws v1.1.0 // indirect 15 | github.com/josharian/intern v1.0.0 // indirect 16 | github.com/mailru/easyjson v0.7.7 // indirect 17 | github.com/pkg/errors v0.9.1 18 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chromedp/cdproto v0.0.0-20220901095120-1a01299a2163 h1:d3i/+z+spo9ieg6L5FWdGmcgvAzsyFNl1vsr68RjzBc= 2 | github.com/chromedp/cdproto v0.0.0-20220901095120-1a01299a2163/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0= 3 | github.com/chromedp/chromedp v0.8.5 h1:HAVg54yQFcn7sg5reVjXtoI1eQaFxhjAjflHACicUFw= 4 | github.com/chromedp/chromedp v0.8.5/go.mod h1:xal2XY5Di7m/bzlGwtoYpmgIOfDqCakOIVg5OfdkPZ4= 5 | github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= 6 | github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= 7 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 8 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 9 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 10 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 11 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= 12 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= 16 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 17 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 18 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= 23 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | -------------------------------------------------------------------------------- /hashtags.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Hashtag is used for getting the media that matches a hashtag on instagram. 9 | type Hashtag struct { 10 | insta *Instagram 11 | err error 12 | 13 | Name string `json:"name"` 14 | ID int64 `json:"id"` 15 | MediaCount int `json:"media_count"` 16 | FormattedMediaCount string `json:"formatted_media_count"` 17 | FollowStatus interface{} `json:"follow_status"` 18 | Subtitle string `json:"subtitle"` 19 | Description string `json:"description"` 20 | Following interface{} `json:"following"` 21 | AllowFollowing interface{} `json:"allow_following"` 22 | AllowMutingStory interface{} `json:"allow_muting_story"` 23 | ProfilePicURL interface{} `json:"profile_pic_url"` 24 | NonViolating interface{} `json:"non_violating"` 25 | RelatedTags interface{} `json:"related_tags"` 26 | DebugInfo interface{} `json:"debug_info"` 27 | // All Top Items 28 | Items []*Item 29 | // All ItemsRecent Items 30 | ItemsRecent []*Item 31 | Story *StoryMedia 32 | NumResults int 33 | 34 | // Sections will always contain the last fetched sections, regardless of tab 35 | Sections []hashtagSection `json:"sections"` 36 | PageInfo map[string]hashtagPageInfo 37 | AutoLoadMoreEnabled bool `json:"auto_load_more_enabled"` 38 | MoreAvailable bool `json:"more_available"` 39 | NextID string `json:"next_max_id"` 40 | NextPage int `json:"next_page"` 41 | NextMediaIds []int64 `json:"next_media_ids"` 42 | Status string `json:"status"` 43 | } 44 | 45 | type hashtagSection struct { 46 | LayoutType string `json:"layout_type"` 47 | LayoutContent struct { 48 | // TODO: misses onebyoneitem etc., like discover page 49 | FillItems []mediaItem `json:"fill_items"` 50 | Medias []mediaItem `json:"medias"` 51 | } `json:"layout_content"` 52 | FeedType string `json:"feed_type"` 53 | ExploreItemInfo struct { 54 | NumColumns int `json:"num_columns"` 55 | TotalNumColumns int `json:"total_num_columns"` 56 | AspectRatio float32 `json:"aspect_ratio"` 57 | Autoplay bool `json:"autoplay"` 58 | } `json:"explore_item_info"` 59 | } 60 | 61 | type mediaItem struct { 62 | Item *Item `json:"media"` 63 | } 64 | 65 | type hashtagPageInfo struct { 66 | MoreAvailable bool `json:"more_available"` 67 | NextID string `json:"next_max_id"` 68 | NextPage int `json:"next_page"` 69 | NextMediaIds []int64 `json:"next_media_ids"` 70 | Status string `json:"status"` 71 | } 72 | 73 | func (h *Hashtag) setValues() { 74 | if h.PageInfo == nil { 75 | h.PageInfo = make(map[string]hashtagPageInfo) 76 | } 77 | 78 | for _, s := range h.Sections { 79 | for _, m := range s.LayoutContent.Medias { 80 | setToItem(m.Item, h) 81 | } 82 | for _, m := range s.LayoutContent.FillItems { 83 | setToItem(m.Item, h) 84 | } 85 | } 86 | } 87 | 88 | // Delete only a place holder, does nothing 89 | func (h *Hashtag) Delete() error { 90 | return nil 91 | } 92 | 93 | func (h *Hashtag) GetNextID() string { 94 | return "" 95 | } 96 | 97 | // NewHashtag returns initialised hashtag structure 98 | // Name parameter is hashtag name 99 | func (insta *Instagram) NewHashtag(name string) *Hashtag { 100 | return &Hashtag{ 101 | insta: insta, 102 | Name: name, 103 | PageInfo: make(map[string]hashtagPageInfo), 104 | } 105 | } 106 | 107 | // Sync wraps Hashtag.Info() 108 | func (h *Hashtag) Sync() error { 109 | return h.Info() 110 | } 111 | 112 | // Info updates Hashtag information 113 | func (h *Hashtag) Info() error { 114 | insta := h.insta 115 | 116 | body, err := insta.sendSimpleRequest(urlTagInfo, h.Name) 117 | if err != nil { 118 | return err 119 | } 120 | err = json.Unmarshal(body, h) 121 | return err 122 | } 123 | 124 | // Next paginates over hashtag top pages. 125 | func (h *Hashtag) Next(p ...interface{}) bool { 126 | return h.next("top") 127 | } 128 | 129 | // NextRecent paginates over hashtag top recent pages. 130 | func (h *Hashtag) NextRecent() bool { 131 | return h.next("recent") 132 | } 133 | 134 | func (h *Hashtag) next(tab string) bool { 135 | pageInfo, ok := h.PageInfo[tab] 136 | 137 | if h.err != nil && h.err != ErrNoMore { 138 | return false 139 | } else if h.err == ErrNoMore && ok && !pageInfo.MoreAvailable { 140 | return false 141 | } 142 | 143 | if tab != "top" && tab != "recent" && tab != "clips" { 144 | h.err = ErrInvalidTab 145 | return false 146 | } 147 | insta := h.insta 148 | name := h.Name 149 | 150 | query := map[string]string{ 151 | "tab": tab, 152 | "_uuid": insta.uuid, 153 | "include_persistent": "false", 154 | "rank_token": insta.rankToken, 155 | } 156 | 157 | if ok { 158 | nextMediaIds, err := json.Marshal(pageInfo.NextMediaIds) 159 | if err != nil { 160 | h.err = err 161 | return false 162 | } 163 | query["max_id"] = pageInfo.NextID 164 | query["page"] = toString(pageInfo.NextPage) 165 | query["next_media_ids"] = string(nextMediaIds) 166 | } 167 | 168 | body, _, err := insta.sendRequest( 169 | &reqOptions{ 170 | Endpoint: fmt.Sprintf(urlTagContent, name), 171 | IsPost: true, 172 | Query: query, 173 | }, 174 | ) 175 | if err != nil { 176 | h.err = err 177 | return false 178 | } 179 | 180 | res := &Hashtag{} 181 | if err := json.Unmarshal(body, res); err != nil { 182 | h.err = err 183 | return false 184 | } 185 | 186 | h.fillItems(res, tab) 187 | if !h.MoreAvailable { 188 | h.err = ErrNoMore 189 | return false 190 | } 191 | return true 192 | } 193 | 194 | func (h *Hashtag) fillItems(res *Hashtag, tab string) { 195 | h.AutoLoadMoreEnabled = res.AutoLoadMoreEnabled 196 | h.NextID = res.NextID 197 | h.MoreAvailable = res.MoreAvailable 198 | h.NextPage = res.NextPage 199 | h.Status = res.Status 200 | h.Sections = res.Sections 201 | 202 | h.PageInfo[tab] = hashtagPageInfo{ 203 | MoreAvailable: res.MoreAvailable, 204 | NextID: res.NextID, 205 | NextPage: res.NextPage, 206 | NextMediaIds: res.NextMediaIds, 207 | } 208 | 209 | h.setValues() 210 | count := 0 211 | for _, s := range res.Sections { 212 | for _, m := range s.LayoutContent.Medias { 213 | count += 1 214 | if tab == "top" { 215 | h.Items = append(h.Items, m.Item) 216 | } else if tab == "recent" { 217 | h.ItemsRecent = append(h.ItemsRecent, m.Item) 218 | } 219 | } 220 | 221 | for _, m := range s.LayoutContent.FillItems { 222 | count += 1 223 | if tab == "top" { 224 | h.Items = append(h.Items, m.Item) 225 | } else if tab == "recent" { 226 | h.ItemsRecent = append(h.ItemsRecent, m.Item) 227 | } 228 | } 229 | } 230 | h.NumResults = count 231 | } 232 | 233 | // Latest will return the last fetched items. 234 | func (h *Hashtag) Latest() []*Item { 235 | var res []*Item 236 | for _, s := range h.Sections { 237 | for _, m := range s.LayoutContent.Medias { 238 | res = append(res, m.Item) 239 | } 240 | } 241 | return res 242 | } 243 | 244 | // Error returns hashtag error 245 | func (h *Hashtag) Error() error { 246 | return h.err 247 | } 248 | 249 | // Clears the Hashtag.err error 250 | func (h *Hashtag) ClearError() { 251 | h.err = nil 252 | } 253 | 254 | func (media *Hashtag) getInsta() *Instagram { 255 | return media.insta 256 | } 257 | 258 | // Stories returns hashtag stories. 259 | func (h *Hashtag) Stories() error { 260 | body, err := h.insta.sendSimpleRequest( 261 | urlTagStories, h.Name, 262 | ) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | var resp struct { 268 | Story *StoryMedia `json:"story"` 269 | Status string `json:"status"` 270 | } 271 | err = json.Unmarshal(body, &resp) 272 | if err != nil { 273 | return err 274 | } 275 | h.Story = resp.Story 276 | return err 277 | } 278 | -------------------------------------------------------------------------------- /headless.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/chromedp/cdproto/cdp" 12 | "github.com/chromedp/cdproto/emulation" 13 | "github.com/chromedp/cdproto/network" 14 | "github.com/chromedp/chromedp" 15 | ) 16 | 17 | type headlessOptions struct { 18 | // seconds 19 | timeout int64 20 | 21 | showBrowser bool 22 | 23 | tasks chromedp.Tasks 24 | } 25 | 26 | // Wait until page gets redirected to instagram home page 27 | func waitForInstagram(b *bool) chromedp.ActionFunc { 28 | return chromedp.ActionFunc( 29 | func(ctx context.Context) error { 30 | for { 31 | select { 32 | case <-time.After(time.Millisecond * 250): 33 | var l string 34 | err := chromedp.Location(&l).Do(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | if l == "https://www.instagram.com/" { 39 | *b = true 40 | return nil 41 | } 42 | case <-ctx.Done(): 43 | return nil 44 | } 45 | } 46 | }) 47 | } 48 | 49 | // Wait until page gets redirected to instagram home page 50 | func printButtons(insta *Instagram) chromedp.Action { 51 | return chromedp.ActionFunc( 52 | func(ctx context.Context) error { 53 | var nodes []*cdp.Node 54 | err := chromedp.Nodes("button", &nodes, chromedp.ByQueryAll).Do(ctx) 55 | if err != nil { 56 | return err 57 | } 58 | for _, p := range nodes { 59 | if len(p.Children) > 0 { 60 | insta.infoHandler( 61 | fmt.Sprintf("Found button on challenge page: %s\n", 62 | p.Children[0].NodeValue, 63 | )) 64 | } 65 | } 66 | return nil 67 | }) 68 | } 69 | 70 | func takeScreenshot(fn string) chromedp.Action { 71 | return chromedp.ActionFunc( 72 | func(ctx context.Context) error { 73 | var buf []byte 74 | err := chromedp.FullScreenshot(&buf, 90).Do(ctx) 75 | if err != nil { 76 | return err 77 | } 78 | if err := os.WriteFile(fn, buf, 0o644); err != nil { 79 | return err 80 | } 81 | return nil 82 | }) 83 | } 84 | 85 | func (insta *Instagram) acceptPrivacyCookies(url string) error { 86 | // Looks for the "Allow All Cookies button" 87 | selector := `//button[contains(text(),"Allow All Cookies")]` 88 | 89 | // This value is not actually used, since its headless, the browser cannot 90 | // be closed easily. If the process is unsuccessful, it will return a timeout error. 91 | success := false 92 | 93 | return insta.runHeadless( 94 | &headlessOptions{ 95 | timeout: 60, 96 | showBrowser: false, 97 | tasks: chromedp.Tasks{ 98 | chromedp.Navigate(url), 99 | 100 | // wait a second after elemnt is visible, does not work otherwise 101 | chromedp.WaitVisible(selector), 102 | chromedp.Sleep(time.Second * 1), 103 | chromedp.Click(selector, chromedp.BySearch), 104 | 105 | waitForInstagram(&success), 106 | }, 107 | }, 108 | ) 109 | } 110 | 111 | func (insta *Instagram) openChallenge(url string) error { 112 | fname := fmt.Sprintf("challenge-screenshot-%d.png", time.Now().Unix()) 113 | 114 | success := false 115 | 116 | err := insta.runHeadless( 117 | &headlessOptions{ 118 | timeout: 300, 119 | showBrowser: true, 120 | tasks: chromedp.Tasks{ 121 | chromedp.Navigate(url), 122 | 123 | // Wait for a few seconds, and screenshot the page after 124 | chromedp.Sleep(time.Second * 5), 125 | printButtons(insta), 126 | takeScreenshot(fname), 127 | 128 | // Wait until page gets redirected to instagram home page 129 | waitForInstagram(&success), 130 | }, 131 | }, 132 | ) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | insta.infoHandler( 138 | fmt.Sprintf( 139 | "Saved a screenshot of the challenge '%s' to %s, please report it in a github issue so the challenge can be solved automatiaclly.\n", 140 | url, 141 | fname, 142 | )) 143 | 144 | if !success { 145 | return ErrChallengeFailed 146 | } 147 | return nil 148 | } 149 | 150 | // runHeadless takes a list of chromedp actions to perform, wrapped around default 151 | // actions that will need to be run for every headless request, such as setting 152 | // the cookies and user-agent. 153 | func (insta *Instagram) runHeadless(options *headlessOptions) error { 154 | if insta.privacyCalled.Get() { 155 | return errors.New("Accept privacy cookie method has already been called. Did it not work? please report on a github issue") 156 | } 157 | 158 | if options.timeout <= 0 { 159 | options.timeout = 60 160 | } 161 | 162 | // Extract required headers as cookies 163 | cookies := map[string]string{} 164 | cookie_list := []string{ 165 | "x-mid", 166 | "authorization", 167 | "ig-u-shbid", 168 | "ig-u-shbts", 169 | "ig-u-ds-user-id", 170 | "ig-u-rur", 171 | } 172 | insta.headerOptions.Range( 173 | func(key, value interface{}) bool { 174 | header := strings.ToLower(key.(string)) 175 | for _, cookie_name := range cookie_list { 176 | if cookie_name == header { 177 | cookies[cookie_name] = value.(string) 178 | } 179 | } 180 | return true 181 | }, 182 | ) 183 | 184 | userAgent := fmt.Sprintf( 185 | "Mozilla/5.0 (Linux; Android %d; %s/%s; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/95.0.4638.50 Mobile Safari/537.36 %s", 186 | insta.device.AndroidRelease, 187 | insta.device.Model, 188 | insta.device.Chipset, 189 | insta.userAgent, 190 | ) 191 | 192 | opts := append( 193 | chromedp.DefaultExecAllocatorOptions[:], 194 | chromedp.UserAgent(userAgent), 195 | ) 196 | 197 | if insta.proxy != "" { 198 | opts = append(opts, chromedp.ProxyServer(insta.proxy)) 199 | } 200 | if insta.proxyInsecure { 201 | opts = append(opts, chromedp.Flag("ignore-certificate-errors", true)) 202 | } 203 | if options.showBrowser { 204 | opts = append(opts, chromedp.Flag("headless", false)) 205 | } 206 | 207 | ctx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) 208 | defer cancel() 209 | 210 | // create chrome instance 211 | ctx, cancel = chromedp.NewContext( 212 | ctx, 213 | // chromedp.WithDebugf(log.Printf), 214 | ) 215 | defer cancel() 216 | 217 | // create a timeout 218 | ctx, cancel = context.WithTimeout(ctx, time.Duration(options.timeout)*time.Second) 219 | defer cancel() 220 | 221 | // Size for custom device 222 | // res := strings.Split(strings.ToLower(insta.device.ScreenResolution), "x") 223 | // width, err := strconv.Atoi(res[0]) 224 | // if err != nil { 225 | // return err 226 | // } 227 | // height, err := strconv.Atoi(res[1]) 228 | // if err != nil { 229 | // return err 230 | // } 231 | 232 | default_actions := chromedp.Tasks{ 233 | // Set custom device type 234 | chromedp.Tasks{ 235 | emulation.SetUserAgentOverride(userAgent), 236 | // emulation.SetDeviceMetricsOverride(int64(width), int64(height), 1.000000, true). 237 | // WithScreenOrientation(&emulation.ScreenOrientation{ 238 | // Type: emulation.OrientationTypePortraitPrimary, 239 | // Angle: 0, 240 | // }), 241 | emulation.SetTouchEmulationEnabled(true), 242 | }, 243 | 244 | // Set custom cookie 245 | chromedp.ActionFunc(func(ctx context.Context) error { 246 | expr := cdp.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour)) 247 | for key, val := range cookies { 248 | err := network.SetCookie(key, val). 249 | WithExpires(&expr). 250 | WithDomain("i.instagram.com"). 251 | // WithHTTPOnly(true). 252 | Do(ctx) 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | return nil 258 | }), 259 | 260 | // Set custom headers 261 | network.Enable(), 262 | network.SetExtraHTTPHeaders( 263 | network.Headers( 264 | map[string]interface{}{ 265 | "X-Requested-With": "com.instagram.android", 266 | }, 267 | ), 268 | ), 269 | } 270 | 271 | err := chromedp.Run(ctx, append(default_actions, options.tasks)) 272 | return err 273 | } 274 | -------------------------------------------------------------------------------- /igtv.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // Do i need to extract the rank token? 10 | 11 | // Methods to create: 12 | // Broadcasts.Discover 13 | 14 | // All items with interface{} I have only seen a null response 15 | type IGTV struct { 16 | insta *Instagram 17 | err error 18 | 19 | // Shared between the endpoints 20 | DestinationClientConfigs interface{} `json:"destination_client_configs"` 21 | MaxID string `json:"max_id"` 22 | MoreAvailable bool `json:"more_available"` 23 | SeenState interface{} `json:"seen_state"` 24 | NumResults int `json:"num_results"` 25 | Status string `json:"status"` 26 | 27 | // Specific to igtv/discover 28 | Badging interface{} `json:"badging"` 29 | BannerToken interface{} `json:"banner_token"` 30 | BrowseItems interface{} `json:"browser_items"` 31 | Channels []IGTVChannel `json:"channels"` 32 | Composer interface{} `json:"composer"` 33 | Items []*Item `json:"items"` 34 | DestinationItems []IGTVItem `json:"destination_items"` 35 | MyChannel struct{} `json:"my_channel"` 36 | 37 | // Specific to igtv/suggested_searches 38 | RankToken int `json:"rank_token"` 39 | } 40 | 41 | // IGTVItem is a media item that can be found inside the IGTV struct, from the 42 | // IGTV Discover endpoint. 43 | type IGTVItem struct { 44 | Title string `json:"title"` 45 | Type string `json:"type"` 46 | Channel IGTVChannel `json:"channel"` 47 | Item *Item `json:"item"` 48 | LogingInfo struct { 49 | SourceChannelType string `json:"source_channel_type"` 50 | } `json:"logging_info"` 51 | 52 | // Specific to igtv/suggested_searches 53 | Hashtag interface{} `json:"hashtag"` 54 | Keyword interface{} `json:"keyword"` 55 | User User `json:"user"` 56 | } 57 | 58 | // IGTVChannel can represent a single user's collection of IGTV posts, or it can 59 | // e.g. represent a user's IGTV series. 60 | // 61 | // It's called a channel, however the Items inside the Channel struct can, but 62 | // don't have to, belong to the same account, depending on the request. It's a bit dubious 63 | // 64 | type IGTVChannel struct { 65 | insta *Instagram 66 | id string // user id parameter 67 | err error 68 | 69 | ApproxTotalVideos interface{} `json:"approx_total_videos"` 70 | ApproxVideosFormatted interface{} `json:"approx_videos_formatted"` 71 | CoverPhotoUrl string `json:"cover_photo_url"` 72 | Description string `json:"description"` 73 | ID string `json:"id"` 74 | Items []*Item `json:"items"` 75 | NumResults int `json:"num_results"` 76 | Broadcasts []*Broadcast `json:"live_items"` 77 | Title string `json:"title"` 78 | Type string `json:"type"` 79 | User *User `json:"user_dict"` 80 | DestinationClientConfigs interface{} `json:"destination_client_configs"` 81 | NextID interface{} `json:"max_id"` 82 | MoreAvailable bool `json:"more_available"` 83 | SeenState interface{} `json:"seen_state"` 84 | } 85 | 86 | func newIGTV(insta *Instagram) *IGTV { 87 | return &IGTV{insta: insta} 88 | } 89 | 90 | // IGTV returns the IGTV items of a user 91 | // 92 | // Use IGTVChannel.Next for pagination. 93 | // 94 | func (user *User) IGTV() (*IGTVChannel, error) { 95 | insta := user.insta 96 | igtv := &IGTVChannel{ 97 | insta: insta, 98 | id: fmt.Sprintf("user_%d", user.ID), 99 | } 100 | if !igtv.Next() { 101 | return igtv, igtv.Error() 102 | } 103 | return igtv, nil 104 | } 105 | 106 | // IGTVSeries will fetch the igtv series of a user. Usually the slice length 107 | // of the return value is 1, as there is one channel, which contains multiple 108 | // series. 109 | // 110 | func (user *User) IGTVSeries() ([]*IGTVChannel, error) { 111 | if !user.HasIGTVSeries { 112 | return nil, ErrIGTVNoSeries 113 | } 114 | insta := user.insta 115 | 116 | body, _, err := insta.sendRequest( 117 | &reqOptions{ 118 | Endpoint: fmt.Sprintf(urlIGTVSeries, user.ID), 119 | Query: generateSignature("{}"), 120 | }, 121 | ) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | var res struct { 127 | Channels []*IGTVChannel `json:"channels"` 128 | Status string `json:"status"` 129 | } 130 | err = json.Unmarshal(body, &res) 131 | if err != nil { 132 | return nil, err 133 | } 134 | for _, chann := range res.Channels { 135 | chann.setValues() 136 | } 137 | return res.Channels, nil 138 | } 139 | 140 | // Live will return a list of current broadcasts 141 | func (igtv *IGTV) Live() (*IGTVChannel, error) { 142 | return igtv.insta.callIGTVChannel("live", "") 143 | } 144 | 145 | // Live test method to see if Live can paginate 146 | func (igtv *IGTVChannel) Live() (*IGTVChannel, error) { 147 | return igtv.insta.callIGTVChannel("live", igtv.GetNextID()) 148 | } 149 | 150 | // GetNexID returns the max id used for pagination. 151 | func (igtv *IGTVChannel) GetNextID() string { 152 | return formatID(igtv.NextID) 153 | } 154 | 155 | // Next allows you to paginate the IGTV feed of a channel. 156 | // returns false when list reach the end. 157 | // if FeedMedia.Error() is ErrNoMore no problems have occurred. 158 | func (igtv *IGTVChannel) Next(params ...interface{}) bool { 159 | if igtv.err != nil { 160 | return false 161 | } 162 | insta := igtv.insta 163 | 164 | query := map[string]string{ 165 | "id": igtv.id, 166 | "_uuid": igtv.insta.uuid, 167 | "count": "10", 168 | } 169 | if len(params)%2 == 0 { 170 | for i := 0; i < len(params); i = i + 2 { 171 | query[params[i].(string)] = params[i+1].(string) 172 | } 173 | } 174 | 175 | if next := igtv.GetNextID(); next != "" { 176 | query["max_id"] = next 177 | } 178 | 179 | body, _, err := insta.sendRequest( 180 | &reqOptions{ 181 | Endpoint: urlIGTVChannel, 182 | IsPost: true, 183 | Query: query, 184 | }) 185 | if err != nil { 186 | igtv.err = err 187 | return false 188 | } 189 | 190 | oldItems := igtv.Items 191 | d := json.NewDecoder(bytes.NewReader(body)) 192 | d.UseNumber() 193 | err = d.Decode(igtv) 194 | if err != nil { 195 | igtv.err = err 196 | return false 197 | } 198 | 199 | if !igtv.MoreAvailable { 200 | igtv.err = ErrNoMore 201 | } 202 | igtv.setValues() 203 | igtv.Items = append(oldItems, igtv.Items...) 204 | return true 205 | } 206 | 207 | // Latest will return the results from the latest fetch 208 | func (igtv *IGTVChannel) Latest() []*Item { 209 | return igtv.Items[len(igtv.Items)-igtv.NumResults:] 210 | } 211 | 212 | func (insta *Instagram) callIGTVChannel(id, nextID string) (*IGTVChannel, error) { 213 | query := map[string]string{ 214 | "id": id, 215 | "_uuid": insta.uuid, 216 | "count": "10", 217 | } 218 | if nextID != "" { 219 | query["max_id"] = nextID 220 | } 221 | body, _, err := insta.sendRequest(&reqOptions{ 222 | Endpoint: urlIGTVChannel, 223 | IsPost: true, 224 | Query: query, 225 | }) 226 | if err != nil { 227 | return nil, err 228 | } 229 | igtv := &IGTVChannel{insta: insta, id: id} 230 | d := json.NewDecoder(bytes.NewReader(body)) 231 | d.UseNumber() 232 | err = d.Decode(igtv) 233 | igtv.setValues() 234 | return igtv, err 235 | } 236 | 237 | func (igtv *IGTVChannel) Delete() error { 238 | return nil 239 | } 240 | 241 | func (igtv *IGTVChannel) Error() error { 242 | return igtv.err 243 | } 244 | 245 | func (media *IGTVChannel) getInsta() *Instagram { 246 | return media.insta 247 | } 248 | 249 | func (igtv *IGTVChannel) setValues() { 250 | insta := igtv.insta 251 | if igtv.User != nil { 252 | igtv.User.insta = insta 253 | } 254 | for _, i := range igtv.Items { 255 | setToItem(i, igtv) 256 | } 257 | for _, br := range igtv.Broadcasts { 258 | br.setValues(insta) 259 | } 260 | } 261 | 262 | // Next allows you to paginate the IGTV Discover page. 263 | func (igtv *IGTV) Next(params ...interface{}) bool { 264 | if igtv.err != nil { 265 | return false 266 | } 267 | insta := igtv.insta 268 | 269 | query := map[string]string{} 270 | if igtv.MaxID != "" { 271 | query["max_id"] = igtv.MaxID 272 | } 273 | body, _, err := insta.sendRequest( 274 | &reqOptions{ 275 | Endpoint: urlIGTVDiscover, 276 | Query: query, 277 | }, 278 | ) 279 | if err != nil { 280 | igtv.err = err 281 | return false 282 | } 283 | 284 | err = json.Unmarshal(body, igtv) 285 | if err != nil { 286 | igtv.err = err 287 | return false 288 | } 289 | igtv.setValues() 290 | count := 0 291 | for _, item := range igtv.DestinationItems { 292 | if item.Item != nil { 293 | igtv.Items = append(igtv.Items, item.Item) 294 | count++ 295 | } 296 | } 297 | igtv.NumResults = count 298 | if !igtv.MoreAvailable { 299 | igtv.err = ErrNoMore 300 | return false 301 | } 302 | return true 303 | } 304 | 305 | func (igtv *IGTV) setValues() { 306 | for _, item := range igtv.DestinationItems { 307 | if item.Item != nil { 308 | setToItem(item.Item, igtv) 309 | } 310 | } 311 | for _, chann := range igtv.Channels { 312 | chann.setValues() 313 | } 314 | } 315 | 316 | // Delete does nothing, is only a place holder 317 | func (igtv *IGTV) Delete() error { 318 | return nil 319 | } 320 | 321 | // Error return the error of IGTV, if one has occured 322 | func (igtv *IGTV) Error() error { 323 | return igtv.err 324 | } 325 | 326 | func (igtv *IGTV) getInsta() *Instagram { 327 | return igtv.insta 328 | } 329 | 330 | // Error return the error of IGTV, if one has occured 331 | func (igtv *IGTV) GetNextID() string { 332 | return igtv.MaxID 333 | } 334 | 335 | // Latest returns the last fetched items, by slicing IGTV.Items with IGTV.NumResults 336 | func (igtv *IGTV) Latest() []*Item { 337 | return igtv.Items[len(igtv.Items)-igtv.NumResults:] 338 | } 339 | -------------------------------------------------------------------------------- /location.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type LocationInstance struct { 9 | insta *Instagram 10 | } 11 | 12 | func newLocation(insta *Instagram) *LocationInstance { 13 | return &LocationInstance{insta: insta} 14 | } 15 | 16 | type LayoutSection struct { 17 | LayoutType string `json:"layout_type"` 18 | LayoutContent struct { 19 | Medias []struct { 20 | Media Item `json:"media"` 21 | } `json:"medias"` 22 | } `json:"layout_content"` 23 | FeedType string `json:"feed_type"` 24 | ExploreItemInfo struct { 25 | NumColumns int `json:"num_columns"` 26 | TotalNumColumns int `json:"total_num_columns"` 27 | AspectRatio float64 `json:"aspect_ratio"` 28 | Autoplay bool `json:"autoplay"` 29 | } `json:"explore_item_info"` 30 | } 31 | 32 | type Section struct { 33 | Sections []LayoutSection `json:"sections"` 34 | MoreAvailable bool `json:"more_available"` 35 | NextPage int `json:"next_page"` 36 | NextMediaIds []int64 `json:"next_media_ids"` 37 | NextID string `json:"next_max_id"` 38 | Status string `json:"status"` 39 | } 40 | 41 | func (l *LocationInstance) Feeds(locationID int64) (*Section, error) { 42 | // TODO: use pagination for location feeds. 43 | insta := l.insta 44 | body, _, err := insta.sendRequest( 45 | &reqOptions{ 46 | Endpoint: fmt.Sprintf(urlFeedLocations, locationID), 47 | IsPost: true, 48 | Query: map[string]string{ 49 | "rank_token": insta.rankToken, 50 | "ranked_content": "true", 51 | "_uid": toString(insta.Account.ID), 52 | "_uuid": insta.uuid, 53 | }, 54 | }, 55 | ) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | section := &Section{} 61 | err = json.Unmarshal(body, section) 62 | return section, err 63 | } 64 | 65 | func (l *Location) Feed() (*Section, error) { 66 | // TODO: use pagination for location feeds. 67 | insta := l.insta 68 | body, _, err := insta.sendRequest( 69 | &reqOptions{ 70 | Endpoint: fmt.Sprintf(urlFeedLocations, l.ID), 71 | IsPost: true, 72 | Query: map[string]string{ 73 | "rank_token": insta.rankToken, 74 | "ranked_content": "true", 75 | "_uid": toString(insta.Account.ID), 76 | "_uuid": insta.uuid, 77 | }, 78 | }, 79 | ) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | section := &Section{} 85 | err = json.Unmarshal(body, section) 86 | return section, err 87 | } 88 | -------------------------------------------------------------------------------- /profiles.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | ) 9 | 10 | // Profiles allows user function interactions 11 | type Profiles struct { 12 | insta *Instagram 13 | } 14 | 15 | // Profile represents an instagram user with their various properties, such as 16 | // their account info, stored in Profile.User (a *User struct), feed, stories, 17 | // Highlights, IGTV posts, and friendship status. 18 | // 19 | type Profile struct { 20 | User *User 21 | Friendship *Friendship 22 | 23 | Feed *FeedMedia 24 | Stories *StoryMedia 25 | Highlights []*Reel 26 | IGTV *IGTVChannel 27 | } 28 | 29 | // VisitProfile will perform the same request sequence as if you visited a profile 30 | // in the app. It will first call Instagram.Search(user), then register the click, 31 | // and lastly visit the profile with User.VisitProfile() and gather (some) posts 32 | // from the user feed, stories, grab the friendship status, and if available IGTV posts. 33 | // 34 | // You can access the profile info from the profile struct by calling Profile.Feed, 35 | // Profile.Stories, Profile.User etc. See the Profile struct for all properties. 36 | // 37 | func (insta *Instagram) VisitProfile(handle string) (*Profile, error) { 38 | sr, err := insta.Search(handle) 39 | if err != nil { 40 | return nil, err 41 | } 42 | for _, r := range sr.Results { 43 | if r.User != nil && r.User.Username == handle { 44 | err = r.RegisterClick() 45 | if err != nil { 46 | return nil, err 47 | } 48 | return r.User.VisitProfile() 49 | } 50 | } 51 | return nil, errors.New("Profile not found") 52 | } 53 | 54 | // VisitProfile will perform the same request sequence as if you visited a profile 55 | // in the app. Thus it will gather (some) posts from the user feed, stories, 56 | // grab the friendship status, and if available IGTV posts. 57 | // 58 | // You can access the profile info from the profile struct by calling Profile.Feed, 59 | // Profile.Stories, Profile.User etc. See the Profile struct for all properties. 60 | // 61 | // This method will visit a profile directly from an already existing User instance. 62 | // To visit a profile without the User struct, you can use Insta.VisitProfile(user), 63 | // which will perform a search, register the click, and call this method. 64 | // 65 | func (user *User) VisitProfile() (*Profile, error) { 66 | p := &Profile{User: user} 67 | 68 | wg := &sync.WaitGroup{} 69 | info := &sync.WaitGroup{} 70 | errChan := make(chan error, 10) 71 | 72 | // Fetch Profile Info 73 | wg.Add(1) 74 | info.Add(1) 75 | go func(wg, info *sync.WaitGroup) { 76 | defer wg.Done() 77 | defer info.Done() 78 | if err := user.Info("entry_point", "profile", "from_module", "blended_search"); err != nil { 79 | errChan <- err 80 | } 81 | }(wg, info) 82 | 83 | // Fetch Friendship 84 | wg.Add(1) 85 | go func(wg *sync.WaitGroup) { 86 | defer wg.Done() 87 | fr, err := user.GetFriendship() 88 | if err != nil { 89 | errChan <- err 90 | } 91 | p.Friendship = fr 92 | }(wg) 93 | 94 | // Fetch Feed 95 | wg.Add(1) 96 | go func(wg *sync.WaitGroup) { 97 | defer wg.Done() 98 | feed := user.Feed() 99 | p.Feed = feed 100 | if !feed.Next() && feed.Error() != ErrNoMore { 101 | errChan <- feed.Error() 102 | } 103 | }(wg) 104 | 105 | // Fetch Stories 106 | wg.Add(1) 107 | go func(wg *sync.WaitGroup) { 108 | defer wg.Done() 109 | stories, err := user.Stories() 110 | p.Stories = stories 111 | if err != nil { 112 | errChan <- err 113 | } 114 | }(wg) 115 | 116 | // Fetch Highlights 117 | wg.Add(1) 118 | go func(wg *sync.WaitGroup) { 119 | defer wg.Done() 120 | h, err := user.Highlights() 121 | p.Highlights = h 122 | if err != nil { 123 | errChan <- err 124 | } 125 | }(wg) 126 | 127 | // Fetch Featured Accounts 128 | wg.Add(1) 129 | go func(wg *sync.WaitGroup) { 130 | defer wg.Done() 131 | _, err := user.GetFeaturedAccounts() 132 | if err != nil { 133 | user.insta.warnHandler(err) 134 | } 135 | }(wg) 136 | 137 | // Fetch IGTV 138 | wg.Add(1) 139 | go func(wg, info *sync.WaitGroup) { 140 | defer wg.Done() 141 | info.Wait() 142 | if user.IGTVCount > 0 { 143 | igtv, err := user.IGTV() 144 | if err != nil { 145 | errChan <- err 146 | } 147 | p.IGTV = igtv 148 | } 149 | }(wg, info) 150 | 151 | wg.Wait() 152 | select { 153 | case err := <-errChan: 154 | return p, err 155 | default: 156 | return p, nil 157 | } 158 | } 159 | 160 | func newProfiles(insta *Instagram) *Profiles { 161 | profiles := &Profiles{ 162 | insta: insta, 163 | } 164 | return profiles 165 | } 166 | 167 | // ByName return a *User structure parsed by username. 168 | // This is not the preffered method to fetch a profile, as the app will 169 | // not simply call this endpoint. It is better to use insta.Search(user), 170 | // or insta.Searchbar.SearchUser(user). 171 | // 172 | func (prof *Profiles) ByName(name string) (*User, error) { 173 | body, err := prof.insta.sendSimpleRequest(urlUserByName, name) 174 | if err == nil { 175 | resp := userResp{} 176 | err = json.Unmarshal(body, &resp) 177 | if err == nil { 178 | user := &resp.User 179 | user.insta = prof.insta 180 | return user, err 181 | } 182 | } 183 | return nil, err 184 | } 185 | 186 | // ByID returns a *User structure parsed by user id. 187 | func (prof *Profiles) ByID(id_ interface{}) (*User, error) { 188 | var id string 189 | switch x := id_.(type) { 190 | case int64: 191 | id = fmt.Sprintf("%d", x) 192 | case int: 193 | id = fmt.Sprintf("%d", x) 194 | case string: 195 | id = x 196 | default: 197 | return nil, errors.New("invalid id, please provide a string or int(64)") 198 | } 199 | 200 | body, _, err := prof.insta.sendRequest( 201 | &reqOptions{ 202 | Endpoint: fmt.Sprintf(urlUserByID, id), 203 | }, 204 | ) 205 | if err == nil { 206 | resp := userResp{} 207 | err = json.Unmarshal(body, &resp) 208 | if err == nil { 209 | user := &resp.User 210 | user.insta = prof.insta 211 | return user, err 212 | } 213 | } 214 | return nil, err 215 | } 216 | 217 | // Blocked returns a list of users you have blocked. 218 | func (prof *Profiles) Blocked() ([]BlockedUser, error) { 219 | body, err := prof.insta.sendSimpleRequest(urlBlockedList) 220 | if err == nil { 221 | resp := blockedListResp{} 222 | err = json.Unmarshal(body, &resp) 223 | return resp.BlockedList, err 224 | } 225 | return nil, err 226 | } 227 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "math/rand" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | type reqOptions struct { 21 | // Connection is connection header. Default is "close". 22 | Connection string 23 | 24 | // Endpoint is the request path of instagram api 25 | Endpoint string 26 | 27 | // Omit API omit the /api/v1/ part of the url 28 | OmitAPI bool 29 | 30 | // IsPost set to true will send request with POST method. 31 | // 32 | // By default this option is false. 33 | IsPost bool 34 | 35 | // Compress post form data with gzip 36 | Gzip bool 37 | 38 | // UseV2 is set when API endpoint uses v2 url. 39 | UseV2 bool 40 | 41 | // Use b.i.instagram.com 42 | Useb bool 43 | 44 | // Query is the parameters of the request 45 | // 46 | // This parameters are independents of the request method (POST|GET) 47 | Query map[string]string 48 | 49 | // DataBytes can be used to pass raw data to a request, instead of a 50 | // form using the Query param. This is used for e.g. photo and vieo uploads. 51 | DataBytes *bytes.Buffer 52 | 53 | // List of headers to ignore 54 | IgnoreHeaders []string 55 | 56 | // Extra headers to add 57 | ExtraHeaders map[string]string 58 | 59 | // Timestamp 60 | Timestamp string 61 | 62 | // Count the number of times the wrapper has been called. 63 | WrapperCount int 64 | 65 | // If Status 429 should be ignored, ErrTooManyRequests. This behaviour should be implemented in 66 | // the wrapper. Goinsta does nothing directly with this value. 67 | Ignore429 bool 68 | } 69 | 70 | func (insta *Instagram) sendSimpleRequest(uri string, a ...interface{}) (body []byte, err error) { 71 | body, _, err = insta.sendRequest( 72 | &reqOptions{ 73 | Endpoint: fmt.Sprintf(uri, a...), 74 | }, 75 | ) 76 | return body, err 77 | } 78 | 79 | func (insta *Instagram) sendRequest(o *reqOptions) (body []byte, h http.Header, err error) { 80 | if insta == nil { 81 | return nil, nil, fmt.Errorf("Error while calling %s: %s", o.Endpoint, ErrInstaNotDefined) 82 | } 83 | 84 | // Check if a challenge is in progress, if so wait for it to complete (with timeout) 85 | if insta.privacyRequested.Get() && !insta.privacyCalled.Get() { 86 | if !insta.checkPrivacy() { 87 | return nil, nil, errors.New("Privacy check timedout") 88 | } 89 | } 90 | 91 | insta.checkXmidExpiry() 92 | 93 | method := "GET" 94 | if o.IsPost { 95 | method = "POST" 96 | } 97 | if o.Connection == "" { 98 | o.Connection = "close" 99 | } 100 | 101 | if o.Timestamp == "" { 102 | o.Timestamp = strconv.FormatInt(time.Now().Unix(), 10) 103 | } 104 | 105 | var nu string 106 | if o.Useb { 107 | nu = instaAPIUrlb 108 | } else { 109 | nu = instaAPIUrl 110 | } 111 | if o.UseV2 && !o.Useb { 112 | nu = instaAPIUrlv2 113 | } else if o.UseV2 && o.Useb { 114 | nu = instaAPIUrlv2b 115 | } 116 | if o.OmitAPI { 117 | nu = baseUrl 118 | o.IgnoreHeaders = append(o.IgnoreHeaders, omitAPIHeadersExclude...) 119 | } 120 | 121 | nu = nu + o.Endpoint 122 | u, err := url.Parse(nu) 123 | if err != nil { 124 | return nil, nil, err 125 | } 126 | 127 | vs := url.Values{} 128 | bf := bytes.NewBuffer([]byte{}) 129 | reqData := bytes.NewBuffer([]byte{}) 130 | 131 | for k, v := range o.Query { 132 | vs.Add(k, v) 133 | } 134 | 135 | // If DataBytes has been passed, use that as data, else use Query 136 | if o.DataBytes != nil { 137 | reqData = o.DataBytes 138 | } else { 139 | reqData.WriteString(vs.Encode()) 140 | } 141 | 142 | var contentEncoding string 143 | if o.IsPost && o.Gzip { 144 | // If gzip encoding needs to be applied 145 | zw := gzip.NewWriter(bf) 146 | defer zw.Close() 147 | if _, err := zw.Write(reqData.Bytes()); err != nil { 148 | return nil, nil, err 149 | } 150 | if err := zw.Close(); err != nil { 151 | return nil, nil, err 152 | } 153 | contentEncoding = "gzip" 154 | } else if o.IsPost { 155 | // use post form if POST request 156 | bf = reqData 157 | } else { 158 | // append query to url if GET request 159 | for k, v := range u.Query() { 160 | vs.Add(k, strings.Join(v, " ")) 161 | } 162 | 163 | u.RawQuery = vs.Encode() 164 | } 165 | 166 | var req *http.Request 167 | req, err = http.NewRequest(method, u.String(), bf) 168 | if err != nil { 169 | return 170 | } 171 | 172 | ignoreHeader := func(h string) bool { 173 | for _, k := range o.IgnoreHeaders { 174 | if k == h { 175 | return true 176 | } 177 | } 178 | return false 179 | } 180 | 181 | setHeaders := func(h map[string]string) { 182 | for k, v := range h { 183 | if v != "" && !ignoreHeader(k) { 184 | req.Header.Set(k, v) 185 | } 186 | } 187 | } 188 | setHeadersAsync := func(key, value interface{}) bool { 189 | k, v := key.(string), value.(string) 190 | if v != "" && !ignoreHeader(k) { 191 | req.Header.Set(k, v) 192 | } 193 | return true 194 | } 195 | 196 | headers := map[string]string{ 197 | "Accept-Language": locale, 198 | "Accept-Encoding": "gzip,deflate", 199 | "Connection": o.Connection, 200 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 201 | "User-Agent": insta.userAgent, 202 | "X-Ig-App-Locale": locale, 203 | "X-Ig-Device-Locale": locale, 204 | "X-Ig-Mapped-Locale": locale, 205 | "X-Ig-App-Id": fbAnalytics, 206 | "X-Ig-Device-Id": insta.uuid, 207 | "X-Ig-Family-Device-Id": insta.fID, 208 | "X-Ig-Android-Id": insta.dID, 209 | "X-Ig-Timezone-Offset": timeOffset, 210 | "X-Ig-Capabilities": igCapabilities, 211 | "X-Ig-Connection-Type": connType, 212 | "X-Pigeon-Session-Id": insta.psID, 213 | "X-Pigeon-Rawclienttime": fmt.Sprintf("%s.%d", o.Timestamp, random(100, 900)), 214 | "X-Ig-Bandwidth-Speed-KBPS": fmt.Sprintf("%d.000", random(1000, 9000)), 215 | "X-Ig-Bandwidth-TotalBytes-B": strconv.FormatInt(random(1000000, 5000000), 10), 216 | "X-Ig-Bandwidth-Totaltime-Ms": strconv.FormatInt(random(200, 800), 10), 217 | "X-Ig-App-Startup-Country": "unkown", 218 | "X-Bloks-Version-Id": bloksVerID, 219 | "X-Bloks-Is-Layout-Rtl": "false", 220 | "X-Bloks-Is-Panorama-Enabled": "true", 221 | "X-Fb-Http-Engine": "Liger", 222 | "X-Fb-Client-Ip": "True", 223 | "X-Fb-Server-Cluster": "True", 224 | } 225 | if insta.Account != nil { 226 | headers["Ig-Intended-User-Id"] = strconv.FormatInt(insta.Account.ID, 10) 227 | } else { 228 | headers["Ig-Intended-User-Id"] = "0" 229 | } 230 | if contentEncoding != "" { 231 | headers["Content-Encoding"] = contentEncoding 232 | } 233 | 234 | setHeaders(headers) 235 | setHeaders(o.ExtraHeaders) 236 | insta.headerOptions.Range(setHeadersAsync) 237 | 238 | resp, err := insta.c.Do(req) 239 | if err != nil { 240 | return nil, nil, err 241 | } 242 | defer resp.Body.Close() 243 | 244 | insta.extractHeaders(resp.Header) 245 | body, err = io.ReadAll(resp.Body) 246 | if err != nil { 247 | return nil, nil, err 248 | } 249 | 250 | // Extract error from request body, if present 251 | err = insta.isError(resp.StatusCode, body, resp.Status, o.Endpoint) 252 | 253 | // Decode gzip encoded responses 254 | encoding := resp.Header.Get("Content-Encoding") 255 | if encoding != "" && encoding == "gzip" { 256 | buf := bytes.NewBuffer(body) 257 | zr, err := gzip.NewReader(buf) 258 | if err != nil { 259 | return nil, nil, err 260 | } 261 | 262 | body, err = io.ReadAll(zr) 263 | if err != nil { 264 | return nil, nil, err 265 | } 266 | 267 | if err := zr.Close(); err != nil { 268 | return nil, nil, err 269 | } 270 | } 271 | 272 | // Log complete response body 273 | if insta.Debug { 274 | r := map[string]interface{}{ 275 | "status": resp.StatusCode, 276 | "endpoint": o.Endpoint, 277 | "body": string(body), 278 | } 279 | 280 | b, err := json.MarshalIndent(r, "", " ") 281 | if err != nil { 282 | return nil, nil, err 283 | } 284 | insta.debugHandler(string(b)) 285 | } 286 | 287 | // Call Request Wrapper 288 | hCopy := resp.Header.Clone() 289 | if insta.reqWrapper != nil { 290 | o.WrapperCount += 1 291 | body, hCopy, err = insta.reqWrapper.GoInstaWrapper( 292 | &ReqWrapperArgs{ 293 | insta: insta, 294 | reqOptions: o, 295 | Body: body, 296 | Headers: hCopy, 297 | Error: err, 298 | }) 299 | } 300 | 301 | return body, hCopy, err 302 | } 303 | 304 | func (insta *Instagram) checkXmidExpiry() { 305 | insta.xmidMu.RLock() 306 | expiry := insta.xmidExpiry 307 | insta.xmidMu.RUnlock() 308 | 309 | t := time.Now().Unix() 310 | if expiry != -1 && t > expiry-10 { 311 | insta.xmidMu.Lock() 312 | insta.xmidExpiry = -1 313 | insta.xmidMu.Unlock() 314 | if err := insta.zrToken(); err != nil { 315 | insta.warnHandler(errors.Wrap(err, "failed to refresh xmid cookie")) 316 | } 317 | } 318 | } 319 | 320 | func (insta *Instagram) extractHeaders(h http.Header) { 321 | extract := func(in string, out string) { 322 | x := h[in] 323 | if len(x) > 0 && x[0] != "" { 324 | // prevent from auth being set without token post login 325 | if in == "Ig-Set-Authorization" { 326 | old, ok := insta.headerOptions.Load(out) 327 | if ok && len(old.(string)) != 0 { 328 | current := strings.Split(old.(string), ":") 329 | newHeader := strings.Split(x[0], ":") 330 | if len(current[2]) > len(newHeader[2]) { 331 | return 332 | } 333 | 334 | } 335 | } 336 | insta.headerOptions.Store(out, x[0]) 337 | } 338 | } 339 | 340 | extract("Ig-Set-Authorization", "Authorization") 341 | extract("Ig-Set-X-Mid", "X-Mid") 342 | extract("X-Ig-Set-Www-Claim", "X-Ig-Www-Claim") 343 | extract("Ig-Set-Ig-U-Ig-Direct-Region-Hint", "Ig-U-Ig-Direct-Region-Hint") 344 | extract("Ig-Set-Ig-U-Shbid", "Ig-U-Shbid") 345 | extract("Ig-Set-Ig-U-Shbts", "Ig-U-Shbts") 346 | extract("Ig-Set-Ig-U-Rur", "Ig-U-Rur") 347 | extract("Ig-Set-Ig-U-Ds-User-Id", "Ig-U-Ds-User-Id") 348 | } 349 | 350 | func (insta *Instagram) checkPrivacy() bool { 351 | d := time.Now().Add(5 * time.Minute) 352 | ctx, cancel := context.WithDeadline(context.Background(), d) 353 | defer cancel() 354 | 355 | for { 356 | select { 357 | case <-time.After(3 * time.Second): 358 | if insta.privacyRequested.Get() { 359 | return true 360 | } 361 | case <-ctx.Done(): 362 | return false 363 | } 364 | } 365 | } 366 | 367 | func (insta *Instagram) isError(code int, body []byte, status, endpoint string) (err error) { 368 | switch code { 369 | case 200: 370 | case 202: 371 | case 400: 372 | ierr := Error400{Endpoint: endpoint} 373 | 374 | // Ignore error, doesn't matter if types don't always match up 375 | json.Unmarshal(body, &ierr) 376 | 377 | switch ierr.GetMessage() { 378 | case "login_required": 379 | if ierr.ErrorTitle == "You've Been Logged Out" { 380 | return ErrLoggedOut 381 | } 382 | return ErrLoginRequired 383 | case "bad_password": 384 | return ErrBadPassword 385 | 386 | case "Sorry, this media has been deleted": 387 | return ErrMediaDeleted 388 | 389 | case "checkpoint_required": 390 | // Usually a request to accept cookies 391 | insta.warnHandler(ierr) 392 | insta.Checkpoint = &ierr.Checkpoint 393 | insta.Checkpoint.insta = insta 394 | return ErrCheckpointRequired 395 | 396 | case "checkpoint_challenge_required": 397 | fallthrough 398 | case "challenge_required": 399 | insta.warnHandler(ierr) 400 | insta.Challenge = ierr.Challenge 401 | insta.Challenge.insta = insta 402 | return ErrChallengeRequired 403 | 404 | case "two_factor_required": 405 | insta.TwoFactorInfo = ierr.TwoFactorInfo 406 | insta.TwoFactorInfo.insta = insta 407 | if insta.Account == nil { 408 | insta.Account = &Account{ 409 | ID: insta.TwoFactorInfo.ID, 410 | Username: insta.TwoFactorInfo.Username, 411 | } 412 | } 413 | return Err2FARequired 414 | case "Please check the code we sent you and try again.": 415 | return ErrInvalidCode 416 | 417 | default: 418 | return ierr 419 | } 420 | 421 | case 403: 422 | ierr := Error400{ 423 | Code: 403, 424 | Endpoint: endpoint, 425 | } 426 | err = json.Unmarshal(body, &ierr) 427 | if err == nil { 428 | switch ierr.Message { 429 | case "login_required": 430 | if ierr.ErrorTitle == "You've Been Logged Out" { 431 | return ErrLoggedOut 432 | } 433 | return ErrLoginRequired 434 | } 435 | return ierr 436 | } 437 | return err 438 | case 429: 439 | return ErrTooManyRequests 440 | case 500: 441 | ierr := ErrorN{ 442 | Endpoint: endpoint, 443 | Status: "500", 444 | Message: string(body), 445 | ErrorType: status, 446 | } 447 | return ierr 448 | case 503: 449 | return Error503{ 450 | Message: "Instagram API error. Try it later.", 451 | } 452 | default: 453 | ierr := ErrorN{ 454 | Endpoint: endpoint, 455 | Status: strconv.Itoa(code), 456 | Message: string(body), 457 | ErrorType: status, 458 | } 459 | json.Unmarshal(body, &ierr) 460 | if ierr.Message == "Transcode not finished yet." { 461 | return nil 462 | } 463 | return ierr 464 | } 465 | return nil 466 | } 467 | 468 | func random(min, max int64) int64 { 469 | rand.Seed(time.Now().UnixNano()) 470 | return rand.Int63n(max-min) + min 471 | } 472 | -------------------------------------------------------------------------------- /shortid.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func leftPad2Len(s string, padStr string, overallLen int) string { 10 | padCountInt := 1 + ((overallLen - len(padStr)) / len(padStr)) 11 | var retStr = strings.Repeat(padStr, padCountInt) + s 12 | return retStr[(len(retStr) - overallLen):] 13 | } 14 | 15 | const base64UrlCharmap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 16 | 17 | func MediaIDFromShortID(code string) (string, error) { 18 | strID := "" 19 | for i := 0; i < len(code); i++ { 20 | base64 := strings.Index(base64UrlCharmap, string(code[i])) 21 | str2bin := strconv.FormatInt(int64(base64), 2) 22 | sixbits := leftPad2Len(str2bin, "0", 6) 23 | strID = strID + sixbits 24 | } 25 | result, err := strconv.ParseInt(strID, 2, 64) 26 | if err != nil { 27 | return "", err 28 | } 29 | return fmt.Sprintf("%d", result), nil 30 | } 31 | -------------------------------------------------------------------------------- /stories.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // StoryReelMention represent story reel mention 10 | type StoryReelMention struct { 11 | X float64 `json:"x"` 12 | Y float64 `json:"y"` 13 | Z int `json:"z"` 14 | Width float64 `json:"width"` 15 | Height float64 `json:"height"` 16 | Rotation float64 `json:"rotation"` 17 | IsPinned int `json:"is_pinned"` 18 | IsHidden int `json:"is_hidden"` 19 | IsSticker int `json:"is_sticker"` 20 | IsFBSticker int `json:"is_fb_sticker"` 21 | User User 22 | DisplayType string `json:"display_type"` 23 | } 24 | 25 | // StoryCTA represent story cta 26 | type StoryCTA struct { 27 | Links []struct { 28 | LinkType int `json:"linkType"` 29 | WebURI string `json:"webUri"` 30 | AndroidClass string `json:"androidClass"` 31 | Package string `json:"package"` 32 | DeeplinkURI string `json:"deeplinkUri"` 33 | CallToActionTitle string `json:"callToActionTitle"` 34 | RedirectURI interface{} `json:"redirectUri"` 35 | LeadGenFormID string `json:"leadGenFormId"` 36 | IgUserID string `json:"igUserId"` 37 | AppInstallObjectiveInvalidationBehavior interface{} `json:"appInstallObjectiveInvalidationBehavior"` 38 | } `json:"links"` 39 | } 40 | 41 | // StoryMedia is the struct that handles the information from the methods to get info about Stories. 42 | type StoryMedia struct { 43 | Reel Reel `json:"reel"` 44 | Broadcast *Broadcast `json:"broadcast"` 45 | Broadcasts []*Broadcast `json:"broadcasts"` 46 | Status string `json:"status"` 47 | } 48 | 49 | // Reel represents a single user's story collection. 50 | // Every user has one reel, and one reel can contain many story items 51 | type Reel struct { 52 | insta *Instagram 53 | 54 | ID interface{} `json:"id"` 55 | Items []*Item `json:"items"` 56 | MediaCount int `json:"media_count"` 57 | MediaIDs []int64 `json:"media_ids"` 58 | Muted bool `json:"muted"` 59 | LatestReelMedia int64 `json:"latest_reel_media"` 60 | LatestBestiesReelMedia float64 `json:"latest_besties_reel_media"` 61 | ExpiringAt float64 `json:"expiring_at"` 62 | Seen float64 `json:"seen"` 63 | SeenRankedPosition int `json:"seen_ranked_position"` 64 | CanReply bool `json:"can_reply"` 65 | CanGifQuickReply bool `json:"can_gif_quick_reply"` 66 | ClientPrefetchScore float64 `json:"client_prefetch_score"` 67 | Title string `json:"title"` 68 | CanReshare bool `json:"can_reshare"` 69 | ReelType string `json:"reel_type"` 70 | ReelMentions []string `json:"reel_mentions"` 71 | PrefetchCount int `json:"prefetch_count"` 72 | // this field can be int or bool 73 | HasBestiesMedia interface{} `json:"has_besties_media"` 74 | HasPrideMedia bool `json:"has_pride_media"` 75 | HasVideo bool `json:"has_video"` 76 | IsCacheable bool `json:"is_cacheable"` 77 | IsSensitiveVerticalAd bool `json:"is_sensitive_vertical_ad"` 78 | RankedPosition int `json:"ranked_position"` 79 | RankerScores struct { 80 | Fp float64 `json:"fp"` 81 | Ptap float64 `json:"ptap"` 82 | Vm float64 `json:"vm"` 83 | } `json:"ranker_scores"` 84 | StoryRankingToken string `json:"story_ranking_token"` 85 | FaceFilterNuxVersion int `json:"face_filter_nux_version"` 86 | HasNewNuxStory bool `json:"has_new_nux_story"` 87 | User User `json:"user"` 88 | } 89 | 90 | // Stories will fetch a user's stories. 91 | func (user *User) Stories() (*StoryMedia, error) { 92 | return user.insta.fetchStories(user.ID) 93 | } 94 | 95 | // Highlights will fetch a user's highlights. 96 | func (user *User) Highlights() ([]*Reel, error) { 97 | data, err := getSupCap() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | body, _, err := user.insta.sendRequest( 103 | &reqOptions{ 104 | Endpoint: fmt.Sprintf(urlUserHighlights, user.ID), 105 | Query: map[string]string{"supported_capabilities_new": data}, 106 | }, 107 | ) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | tray := &Tray{} 113 | err = json.Unmarshal(body, &tray) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | tray.set(user.insta) 119 | return tray.Stories, nil 120 | } 121 | 122 | // Deletes ALL user's instagram stories. 123 | // If you want to remove a single story, pick one from StoryMedia.Items, and 124 | // call Item.Delete() 125 | // 126 | // See example: examples/media/deleteStories.go 127 | func (media *Reel) Delete() error { 128 | // TODO: update to reel 129 | for _, item := range media.Items { 130 | err := item.Delete() 131 | if err != nil { 132 | return err 133 | } 134 | time.Sleep(200 * time.Millisecond) 135 | 136 | } 137 | return nil 138 | } 139 | 140 | func (media *StoryMedia) setValues(insta *Instagram) { 141 | media.Reel.setValues(insta) 142 | if media.Broadcast != nil { 143 | media.Broadcast.setValues(insta) 144 | } 145 | for _, br := range media.Broadcasts { 146 | br.setValues(insta) 147 | } 148 | } 149 | 150 | func (media *Reel) setValues(insta *Instagram) { 151 | media.insta = insta 152 | media.User.insta = insta 153 | for _, i := range media.Items { 154 | i.insta = insta 155 | i.User.insta = insta 156 | } 157 | } 158 | 159 | // Seen marks story as seen. 160 | /* 161 | func (media *StoryMedia) Seen() error { 162 | insta := media.inst 163 | data, err := insta.prepareData( 164 | map[string]interface{}{ 165 | "container_module": "feed_timeline", 166 | "live_vods_skipped": "", 167 | "nuxes_skipped": "", 168 | "nuxes": "", 169 | "reels": "", // TODO xd 170 | "live_vods": "", 171 | "reel_media_skipped": "", 172 | }, 173 | ) 174 | if err == nil { 175 | _, _, err = insta.sendRequest( 176 | &reqOptions{ 177 | Endpoint: urlMediaSeen, // reel=1&live_vod=0 178 | Query: generateSignature(data), 179 | IsPost: true, 180 | UseV2: true, 181 | }, 182 | ) 183 | } 184 | return err 185 | } 186 | */ 187 | 188 | type trayRequest struct { 189 | Name string `json:"name"` 190 | Value string `json:"value"` 191 | } 192 | 193 | func (insta *Instagram) fetchStories(id int64) (*StoryMedia, error) { 194 | supCap, err := getSupCap() 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | body, _, err := insta.sendRequest( 200 | &reqOptions{ 201 | Endpoint: fmt.Sprintf(urlUserStories, id), 202 | Query: map[string]string{"supported_capabilities_new": supCap}, 203 | }, 204 | ) 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | m := &StoryMedia{} 210 | err = json.Unmarshal(body, m) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | m.setValues(insta) 216 | return m, nil 217 | } 218 | 219 | // Sync function is used when Highlights must be sync. 220 | // Highlight must be sync when User.Highlights does not return any object inside StoryMedia slice. 221 | // 222 | // This function does NOT update Stories items. 223 | // 224 | // This function updates (fetches) StoryMedia.Items 225 | func (media *Reel) Sync() error { 226 | if media.ReelType != "highlight_reel" { 227 | return ErrNotHighlight 228 | } 229 | 230 | insta := media.insta 231 | supCap, err := getSupCap() 232 | if err != nil { 233 | return err 234 | } 235 | 236 | id := media.ID.(string) 237 | data, err := json.Marshal( 238 | map[string]interface{}{ 239 | "exclude_media_ids": "[]", 240 | "supported_capabilities_new": supCap, 241 | "source": "reel_feed_timeline", 242 | "_uid": toString(insta.Account.ID), 243 | "_uuid": insta.uuid, 244 | "user_ids": []string{id}, 245 | }, 246 | ) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | body, _, err := insta.sendRequest( 252 | &reqOptions{ 253 | Endpoint: urlReelMedia, 254 | IsPost: true, 255 | Query: generateSignature(data), 256 | }, 257 | ) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | resp := trayResp{} 263 | err = json.Unmarshal(body, &resp) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | m, ok := resp.Reels[id] 269 | if ok { 270 | *media = m 271 | media.setValues(insta) 272 | return nil 273 | } 274 | return fmt.Errorf("cannot find %s structure in response", id) 275 | } 276 | -------------------------------------------------------------------------------- /tests/account_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Davincible/goinsta/v3" 7 | ) 8 | 9 | func TestPendingFriendships(t *testing.T) { 10 | insta, err := goinsta.EnvRandAcc() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | t.Logf("Logged in as %s\n", insta.Account.Username) 15 | 16 | count, err := insta.Account.PendingRequestCount() 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if count == 0 { 21 | t.Skip("No pending friend requests found") 22 | } 23 | t.Logf("Found %d pending frienships\n", count) 24 | 25 | result, err := insta.Account.PendingFollowRequests() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | pending := result.Users 30 | 31 | err = pending[0].ApprovePending() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | t.Logf("Approved request for %s\n", pending[0].Username) 36 | 37 | if len(pending) >= 2 { 38 | err = pending[1].IgnorePending() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | t.Logf("Ignored request for %s\n", pending[1].Username) 43 | } 44 | count, err = insta.Account.PendingRequestCount() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | t.Logf("After approving there are %d pending friendships remaining\n", count) 49 | } 50 | 51 | func TestFollowList(t *testing.T) { 52 | insta, err := goinsta.EnvRandAcc() 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | t.Logf("Logged in as %s\n", insta.Account.Username) 57 | 58 | users := insta.Account.Following("", goinsta.DefaultOrder) 59 | for users.Next() { 60 | t.Logf("Fetched %d following", len(users.Users)) 61 | } 62 | t.Logf("Fetched %d following", len(users.Users)) 63 | if users.Error() != goinsta.ErrNoMore { 64 | t.Fatal(users.Error()) 65 | } 66 | 67 | users = insta.Account.Following("", goinsta.LatestOrder) 68 | for users.Next() { 69 | t.Logf("Fetched %d following (latest order)", len(users.Users)) 70 | } 71 | t.Logf("Fetched %d following (latest order)", len(users.Users)) 72 | if users.Error() != goinsta.ErrNoMore { 73 | t.Fatal(users.Error()) 74 | } 75 | 76 | users = insta.Account.Followers("") 77 | for users.Next() { 78 | t.Logf("Fetched %d followers", len(users.Users)) 79 | } 80 | t.Logf("Fetched %d followers", len(users.Users)) 81 | if users.Error() != goinsta.ErrNoMore { 82 | t.Fatal(users.Error()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/feed_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Davincible/goinsta/v3" 9 | ) 10 | 11 | func TestFeedUser(t *testing.T) { 12 | insta, err := goinsta.EnvRandAcc() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Logf("Logged in as %s\n", insta.Account.Username) 17 | 18 | sr, err := insta.Searchbar.SearchUser("elonrmuskk") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | user := sr.Users[0] 23 | feed := user.Feed() 24 | 25 | next := feed.NextID 26 | for i := 0; feed.Next(); i++ { 27 | t.Logf("Fetched feed page %d/5", i) 28 | if feed.NextID == next { 29 | t.Fatal("Next ID must be different after each request") 30 | } 31 | if feed.Status != "ok" { 32 | t.Fatalf("Status not ok: %s\n", feed.Status) 33 | } 34 | 35 | if err := feed.GetCommentInfo(); err != nil { 36 | t.Fatalf("Failed to fetch comment info: %v", err) 37 | } 38 | 39 | next = feed.NextID 40 | if i == 5 { 41 | break 42 | } 43 | time.Sleep(time.Duration(rand.Intn(10)) * time.Second) 44 | } 45 | 46 | t.Logf("Gathered %d posts, %d on last request\n", len(feed.Items), feed.NumResults) 47 | } 48 | 49 | func TestFeedDiscover(t *testing.T) { 50 | insta, err := goinsta.EnvRandAcc() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | t.Logf("Logged in as %s\n", insta.Account.Username) 55 | 56 | feed := insta.Discover 57 | next := feed.NextID 58 | 59 | outside: 60 | for i := 0; feed.Next(); i++ { 61 | if feed.NextID == next { 62 | t.Fatal("Next ID must be different after each request") 63 | } 64 | if feed.Status != "ok" { 65 | t.Fatalf("Status not ok: %s\n", feed.Status) 66 | } 67 | next = feed.NextID 68 | if i == 5 { 69 | break outside 70 | } 71 | time.Sleep(time.Duration(rand.Intn(10)) * time.Second) 72 | } 73 | 74 | t.Logf("Gathered %d posts, %d on last request\n", len(feed.Items), feed.NumResults) 75 | } 76 | 77 | func TestFeedTagLike(t *testing.T) { 78 | insta, err := goinsta.EnvRandAcc() 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | t.Logf("Logged in as %s\n", insta.Account.Username) 83 | hashtag := insta.NewHashtag("golang") 84 | err = hashtag.Info() 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | // First round 90 | if s := hashtag.Next(); !s { 91 | t.Fatal(hashtag.Error()) 92 | } 93 | 94 | if len(hashtag.Items) == 0 { 95 | t.Logf("%+v", hashtag.Sections) 96 | t.Fatalf("Items length is 0, section length is %d\n", len(hashtag.Sections)) 97 | } 98 | t.Logf("Found %d posts", len(hashtag.Items)) 99 | 100 | for i, item := range hashtag.Items { 101 | err = item.Like() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | t.Logf("media %s liked by goinsta", item.ID) 106 | if i == 5 { 107 | break 108 | } 109 | time.Sleep(3 * time.Second) 110 | } 111 | } 112 | 113 | func TestFeedTagNextOld(t *testing.T) { 114 | insta, err := goinsta.EnvRandAcc() 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | t.Logf("Logged in as %s\n", insta.Account.Username) 119 | feedTag, err := insta.Feed.Tags("golang") 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | initNextID := feedTag.NextID 125 | success := feedTag.Next() 126 | if !success { 127 | t.Fatal("Failed to fetch next page") 128 | } 129 | gotStatus := feedTag.Status 130 | 131 | if gotStatus != "ok" { 132 | t.Errorf("Status = %s; want ok", gotStatus) 133 | } 134 | 135 | gotNextID := feedTag.NextID 136 | if gotNextID == initNextID { 137 | t.Errorf("NextID must differ after FeedTag.Next() call") 138 | } 139 | t.Logf("Fetched %d posts", len(feedTag.Items)) 140 | } 141 | 142 | func TestFeedTagNext(t *testing.T) { 143 | insta, err := goinsta.EnvRandAcc() 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | t.Logf("Logged in as %s\n", insta.Account.Username) 148 | hashtag := insta.NewHashtag("golang") 149 | err = hashtag.Info() 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | 154 | // First round 155 | if s := hashtag.Next(); !s { 156 | t.Fatal(hashtag.Error()) 157 | } 158 | 159 | initNextID := hashtag.NextID 160 | 161 | // Second round 162 | if s := hashtag.Next(); !s { 163 | t.Fatal(hashtag.Error()) 164 | } 165 | 166 | if hashtag.Status != "ok" { 167 | t.Errorf("Status = %s; want ok", hashtag.Status) 168 | } 169 | 170 | gotNextID := hashtag.NextID 171 | if gotNextID == initNextID { 172 | t.Errorf("NextID must differ after FeedTag.Next() call") 173 | } 174 | t.Logf("Fetched %d posts", len(hashtag.Items)) 175 | } 176 | 177 | func TestFeedTagNextRecent(t *testing.T) { 178 | insta, err := goinsta.EnvRandAcc() 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | t.Logf("Logged in as %s\n", insta.Account.Username) 183 | hashtag := insta.NewHashtag("golang") 184 | err = hashtag.Info() 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | // First round 190 | if s := hashtag.NextRecent(); !s { 191 | t.Fatal(hashtag.Error()) 192 | } 193 | 194 | initNextID := hashtag.NextID 195 | 196 | // Second round 197 | if s := hashtag.NextRecent(); !s { 198 | t.Fatal(hashtag.Error()) 199 | } 200 | 201 | if hashtag.Status != "ok" { 202 | t.Errorf("Status = %s; want ok", hashtag.Status) 203 | } 204 | 205 | gotNextID := hashtag.NextID 206 | if gotNextID == initNextID { 207 | t.Errorf("NextID must differ after FeedTag.Next() call") 208 | } 209 | t.Logf("Fetched %d posts", len(hashtag.ItemsRecent)) 210 | } 211 | -------------------------------------------------------------------------------- /tests/igtv_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Davincible/goinsta/v3" 8 | ) 9 | 10 | func TestIGTVChannel(t *testing.T) { 11 | insta, err := goinsta.EnvRandAcc() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | t.Logf("Logged in as %s\n", insta.Account.Username) 16 | 17 | user, err := insta.Profiles.ByName("f1") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | channel, err := user.IGTV() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | t.Logf("Fetched %d IGTV items", len(channel.Items)) 27 | 28 | if !channel.Next() { 29 | err := channel.Error() 30 | if err == goinsta.ErrNoMore { 31 | t.Logf("No more posts available (%v)", channel.MoreAvailable) 32 | return 33 | } 34 | t.Fatal(channel.Error()) 35 | } 36 | t.Logf("Fetched %d IGTV items", len(channel.Items)) 37 | } 38 | 39 | func TestIGTVSeries(t *testing.T) { 40 | insta, err := goinsta.EnvRandAcc() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | t.Logf("Logged in as %s\n", insta.Account.Username) 45 | 46 | user, err := insta.Profiles.ByName("kimkotter") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | series, err := user.IGTVSeries() 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | total := 0 56 | for _, serie := range series { 57 | total += len(serie.Items) 58 | } 59 | t.Logf("Found %d series. Containing %d IGTV posts in total.", len(series), total) 60 | } 61 | 62 | func TestIGTVLive(t *testing.T) { 63 | insta, err := goinsta.EnvRandAcc() 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | t.Logf("Logged in as %s\n", insta.Account.Username) 68 | 69 | igtv, err := insta.IGTV.Live() 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | broadcasts := igtv.Broadcasts 75 | t.Logf("Found %d broadcasts. More Available = %v\n", len(broadcasts), igtv.MoreAvailable) 76 | if len(broadcasts) == 0 { 77 | t.Fatal("No broadcasts found") 78 | } 79 | 80 | if igtv.MoreAvailable { 81 | time.Sleep(3 * time.Second) 82 | igtv, err := igtv.Live() 83 | if err != nil { 84 | t.Error(err) 85 | } else { 86 | t.Logf("Found %d broadcasts. More Available = %v\n", len(igtv.Broadcasts), igtv.MoreAvailable) 87 | } 88 | } 89 | 90 | for _, br := range broadcasts { 91 | t.Logf("Broadcast by %s has %d viewers\n", br.User.Username, int(br.ViewerCount)) 92 | } 93 | 94 | br := broadcasts[0] 95 | if err := br.GetInfo(); err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | comments, err := br.GetComments() 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | t.Logf("Comment Count: %d\n", comments.CommentCount) 104 | 105 | likes, err := br.GetLikes() 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | t.Logf("Like Count: %d\n", likes.Likes) 110 | 111 | heartbeat, err := br.GetHeartbeat() 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | t.Logf("Viewer Count: %d\n", int(heartbeat.ViewerCount)) 116 | } 117 | 118 | func TestIGTVDiscover(t *testing.T) { 119 | t.Skip("Skipping IGTV Discover, depricated") 120 | 121 | insta, err := goinsta.EnvRandAcc() 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | t.Logf("Logged in as %s\n", insta.Account.Username) 126 | 127 | for i := 0; i < 5; i++ { 128 | if !insta.IGTV.Next() { 129 | t.Fatal(insta.IGTV.Error()) 130 | } 131 | t.Logf("Fetched %d posts", len(insta.IGTV.Items)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/inbox_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/Davincible/goinsta/v3" 9 | ) 10 | 11 | // Random big accounts used for story reply and DM tests 12 | var possibleUsers = []string{ 13 | "kimkardashian", 14 | "kyliejenner", 15 | "kendaljenner", 16 | "addisonraee", 17 | "iamcardib", 18 | "snoopdogg", 19 | "dualipa", 20 | "stassiebaby", 21 | "kourtneykardash", 22 | "f1", 23 | "madscandids", 24 | "9gag", 25 | } 26 | 27 | func TestStoryReply(t *testing.T) { 28 | insta, err := goinsta.EnvRandAcc() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | t.Logf("Logged in as %s\n", insta.Account.Username) 33 | insta.SetWarnHandler(t.Log) 34 | 35 | for _, acc := range possibleUsers { 36 | user, err := insta.Profiles.ByName(acc) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | stories, err := user.Stories() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | for _, story := range stories.Reel.Items { 45 | if story.CanReply { 46 | err := story.Reply("Nice! :)") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | t.Logf("Replied to a story of %s", acc) 51 | return 52 | } 53 | } 54 | } 55 | t.Fatal("Unable to find a story to reply to") 56 | } 57 | 58 | func TestInboxSync(t *testing.T) { 59 | insta, err := goinsta.EnvRandAcc() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | t.Logf("Logged in as %s\n", insta.Account.Username) 64 | insta.SetWarnHandler(t.Log) 65 | 66 | if !insta.Inbox.InitialSnapshot() && insta.Inbox.Error() != goinsta.ErrNoMore { 67 | t.Fatal(insta.Inbox.Error()) 68 | } 69 | 70 | if err := insta.Inbox.Sync(); err != nil { 71 | t.Fatal(err) 72 | } 73 | t.Logf("Fetched %d conversations", len(insta.Inbox.Conversations)) 74 | } 75 | 76 | func TestInboxNew(t *testing.T) { 77 | insta, err := goinsta.EnvRandAcc() 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | t.Logf("Logged in as %s\n", insta.Account.Username) 82 | insta.SetWarnHandler(t.Log) 83 | 84 | rand.Seed(time.Now().UnixNano()) 85 | randUser := possibleUsers[rand.Intn(len(possibleUsers))] 86 | user, err := insta.Profiles.ByName(randUser) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | conv, err := insta.Inbox.New(user, "Roses are red, violets are blue, cushions are soft, and so are you") 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err := conv.Send("Feeling poetic today uknow"); err != nil { 97 | t.Fatal(err) 98 | } 99 | t.Logf("DM'ed %s", randUser) 100 | } 101 | -------------------------------------------------------------------------------- /tests/login_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Davincible/goinsta/v3" 7 | ) 8 | 9 | func TestImportAccount(t *testing.T) { 10 | // Test Import 11 | insta, err := goinsta.EnvRandAcc() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | if err := insta.OpenApp(); err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | t.Logf("logged into Instagram as user '%s'", insta.Account.Username) 20 | logPosts(t, insta) 21 | } 22 | 23 | func TestLogin(t *testing.T) { 24 | // Test Login 25 | user, pass, err := goinsta.EnvRandLogin() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | t.Logf("Attempting to login as %s\n", user) 30 | if user == "codebrewernl" { 31 | t.Skip() 32 | } 33 | 34 | insta := goinsta.New(user, pass) 35 | if err = insta.Login(); err != nil { 36 | t.Fatal(err) 37 | } 38 | t.Log("Logged in successfully") 39 | logPosts(t, insta) 40 | 41 | // Test Logout 42 | if err := insta.Logout(); err != nil { 43 | t.Fatal(err) 44 | } 45 | t.Log("Logged out successfully") 46 | } 47 | 48 | func logPosts(t *testing.T, insta *goinsta.Instagram) { 49 | t.Logf("Gathered %d Timeline posts, %d Stories, %d Discover items, %d Notifications", 50 | len(insta.Timeline.Items), 51 | len(insta.Timeline.Tray.Stories), 52 | len(insta.Discover.Items), 53 | len(insta.Activity.NewStories)+len(insta.Activity.OldStories), 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /tests/profile_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Davincible/goinsta/v3" 7 | ) 8 | 9 | func TestProfileVisit(t *testing.T) { 10 | insta, err := goinsta.EnvRandAcc() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | t.Logf("Logged in as %s\n", insta.Account.Username) 15 | 16 | profile, err := insta.VisitProfile("miakhalifa") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | t.Logf("Gatherd %d posts, %d story items, %d highlights", 22 | len(profile.Feed.Items), 23 | len(profile.Stories.Reel.Items), 24 | len(profile.Highlights), 25 | ) 26 | } 27 | 28 | func TestProfilesByName(t *testing.T) { 29 | insta, err := goinsta.EnvRandAcc() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | t.Logf("Logged in as %s\n", insta.Account.Username) 34 | 35 | _, err = insta.Profiles.ByName("binance") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | func TestProfilesByID(t *testing.T) { 42 | insta, err := goinsta.EnvRandAcc() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | t.Logf("Logged in as %s\n", insta.Account.Username) 47 | 48 | _, err = insta.Profiles.ByID("28527810") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | } 53 | 54 | func TestProfilesBlocked(t *testing.T) { 55 | insta, err := goinsta.EnvRandAcc() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | t.Logf("Logged in as %s\n", insta.Account.Username) 60 | 61 | blocked, err := insta.Profiles.Blocked() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | t.Logf("Foud %d blocked users", len(blocked)) 67 | } 68 | -------------------------------------------------------------------------------- /tests/search_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "testing" 7 | 8 | "github.com/Davincible/goinsta/v3" 9 | ) 10 | 11 | func TestSearchUser(t *testing.T) { 12 | insta, err := goinsta.EnvRandAcc() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Logf("Logged in as %s\n", insta.Account.Username) 17 | 18 | // Search for users 19 | result, err := insta.Searchbar.SearchUser("nicky") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if result.Status != "ok" { 24 | t.Fatal(result.Status) 25 | } 26 | t.Logf("Result length is %d", len(result.Users)) 27 | 28 | // Select a random user 29 | if len(result.Users) == 0 { 30 | t.Fatal("No search results found! Change search query or fix api") 31 | } 32 | user := result.Users[rand.Intn(len(result.Users))] 33 | err = result.RegisterUserClick(user) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | // Get user info 39 | err = user.Info() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | // Get user feed 45 | feed := user.Feed() 46 | if !feed.Next() { 47 | t.Fatalf("Failed to get feed: %s", feed.Error()) 48 | } 49 | t.Logf("Found %d posts", len(feed.Items)) 50 | 51 | // Err if no posts are found 52 | if len(feed.Items) == 0 && user.MediaCount != 0 { 53 | t.Fatal("Failed to fetch any posts while the user does have posts") 54 | } else if len(feed.Items) != 0 { 55 | // Like a random post to make sure the insta pointer is set and working 56 | post := feed.Items[rand.Intn(len(feed.Items))] 57 | err := post.Like() 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | } 62 | } 63 | 64 | func TestSearchHashtag(t *testing.T) { 65 | insta, err := goinsta.EnvRandAcc() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | t.Logf("Logged in as %s\n", insta.Account.Username) 70 | 71 | // Search for hashtags 72 | query := "photography" 73 | result, err := insta.Searchbar.SearchHashtag(query) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if result.Status != "ok" { 78 | t.Fatal(result.Status) 79 | } 80 | if len(result.Tags) == 0 { 81 | t.Fatal("No results found") 82 | } 83 | t.Logf("Result length is %d", len(result.Tags)) 84 | 85 | var hashtag *goinsta.Hashtag 86 | for _, tag := range result.Tags { 87 | if tag.Name == query { 88 | result.RegisterHashtagClick(tag) 89 | hashtag = tag 90 | break 91 | } 92 | } 93 | 94 | for i := 0; i < 5; i++ { 95 | if !hashtag.Next() { 96 | t.Fatal(hashtag.Error()) 97 | } 98 | t.Logf("Fetched %d posts", len(hashtag.Items)) 99 | } 100 | 101 | for i := 0; i < 5; i++ { 102 | if !hashtag.NextRecent() { 103 | t.Log(hashtag.Error()) 104 | } 105 | t.Logf("Fetched %d recent posts", len(hashtag.ItemsRecent)) 106 | } 107 | } 108 | 109 | func TestSearchLocation(t *testing.T) { 110 | insta, err := goinsta.EnvRandAcc() 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | t.Logf("Logged in as %s\n", insta.Account.Username) 115 | 116 | // Search for hashtags 117 | result, err := insta.Searchbar.SearchLocation("New York") 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | if result.Status != "ok" { 122 | t.Fatal(errors.New(result.Status)) 123 | } 124 | t.Logf("Result length is %d", len(result.Places)) 125 | 126 | location := result.Places[0].Location 127 | feed, err := location.Feed() 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | t.Logf("Found %d sections", len(feed.Sections)) 132 | } 133 | -------------------------------------------------------------------------------- /tests/shortid_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Davincible/goinsta/v3" 7 | ) 8 | 9 | func TestMediaIDFromShortID(t *testing.T) { 10 | mediaID, err := goinsta.MediaIDFromShortID("BR_repxhx4O") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | if mediaID != "1477090425239445006" { 15 | t.Fatal("Invalid mediaID") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/sysacc_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "github.com/Davincible/goinsta/v3" 17 | ) 18 | 19 | var errNoAPIKEY = errors.New("No Pixabay API Key has been found. Please add one to .env as PIXABAY_API_KEY") 20 | 21 | type pixaBayRes struct { 22 | Total int `json:"total"` 23 | TotalHits int `json:"totalHits"` 24 | Hits []struct { 25 | ID int `json:"id"` 26 | URL string `json:"page_url"` 27 | Type string `json:"type"` 28 | Tags string `json:"tags"` 29 | Duration int `json:"duration"` 30 | PictureID string `json:"picture_id"` 31 | Videos struct { 32 | Large video `json:"large"` 33 | Medium video `json:"medium"` 34 | Small video `json:"small"` 35 | Tiny video `json:"tiny"` 36 | } `json:"videos"` 37 | } `json:"hits"` 38 | } 39 | 40 | type video struct { 41 | URL string `json:"url"` 42 | Width int `json:"width"` 43 | Height int `json:"height"` 44 | Size int `json:"size"` 45 | Content []byte 46 | } 47 | 48 | func TestEnvLoadAccs(t *testing.T) { 49 | accs, err := goinsta.EnvLoadAccs() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | if len(accs) == 0 { 55 | t.Fatalf("No accounts found") 56 | } 57 | t.Logf("Found %d accounts", len(accs)) 58 | } 59 | 60 | func loadEnv() ([]string, error) { 61 | environ := os.Environ() 62 | env, err := dotenv() 63 | if err != nil { 64 | return nil, err 65 | } 66 | environ = append(environ, env...) 67 | return environ, nil 68 | } 69 | 70 | func dotenv() ([]string, error) { 71 | file, err := os.Open(".env") 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer file.Close() 76 | 77 | buf := new(bytes.Buffer) 78 | _, err = buf.ReadFrom(file) 79 | if err != nil { 80 | return nil, err 81 | } 82 | env := strings.Split(string(buf.Bytes()), "\n") 83 | return env, nil 84 | } 85 | 86 | func Error(t *testing.T, err error) { 87 | t.Log(err.Error()) 88 | t.Fatal(err) 89 | } 90 | 91 | func getPixabayAPIKey() (string, error) { 92 | environ, err := loadEnv() 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | for _, env := range environ { 98 | if strings.HasPrefix(env, "PIXABAY_API_KEY") { 99 | key := strings.Split(env, "=")[1] 100 | if key[0] == '"' { 101 | key = key[1 : len(key)-1] 102 | } 103 | return key, nil 104 | } 105 | } 106 | return "", errNoAPIKEY 107 | } 108 | 109 | func getPhoto(width, height int, i ...int) (io.Reader, error) { 110 | url := fmt.Sprintf("https://picsum.photos/%d/%d", width, height) 111 | resp, err := http.Get(url) 112 | if err != nil { 113 | return nil, err 114 | } 115 | defer resp.Body.Close() 116 | 117 | if resp.StatusCode != 200 { 118 | // Retry on failure 119 | if len(i) == 0 || i[0] < 5 { 120 | c := 0 121 | if len(i) > 0 { 122 | c = i[0] + 1 123 | } 124 | fmt.Println("Failed to get photo, retrying...") 125 | time.Sleep(5 * time.Second) 126 | return getPhoto(width, height, c) 127 | } 128 | return nil, fmt.Errorf("Get image status code %d", resp.StatusCode) 129 | } 130 | 131 | buf := new(bytes.Buffer) 132 | _, err = buf.ReadFrom(resp.Body) 133 | 134 | return buf, err 135 | } 136 | 137 | func getVideo(o ...map[string]interface{}) (*video, error) { 138 | key, err := getPixabayAPIKey() 139 | if err != nil { 140 | return nil, err 141 | } 142 | url := fmt.Sprintf("https://pixabay.com/api/videos/?key=%s&per_page=200", key) 143 | 144 | // Get video list 145 | resp, err := http.Get(url) 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | body, err := io.ReadAll(resp.Body) 151 | if err != nil { 152 | return nil, err 153 | } 154 | if err := resp.Body.Close(); err != nil { 155 | return nil, err 156 | } 157 | 158 | var res pixaBayRes 159 | err = json.Unmarshal(body, &res) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | // Select random video 165 | max_length := 0 166 | if len(o) > 0 { 167 | options := o[0] 168 | max_length, _ = options["max_length"].(int) 169 | } 170 | 171 | valid := false 172 | var vid video 173 | for !valid { 174 | rand.Seed(time.Now().UnixNano()) 175 | r := rand.Intn(len(res.Hits)) 176 | vid = res.Hits[r].Videos.Small 177 | if max_length == 0 || res.Hits[r].Duration < max_length { 178 | valid = true 179 | } 180 | } 181 | 182 | // Download video 183 | resp, err = http.Get(vid.URL) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | video, err := io.ReadAll(resp.Body) 189 | if err != nil { 190 | return nil, err 191 | } 192 | if err := resp.Body.Close(); err != nil { 193 | return nil, err 194 | } 195 | vid.Content = video 196 | return &vid, nil 197 | } 198 | -------------------------------------------------------------------------------- /tests/timeline_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "math/rand" 5 | "path" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/Davincible/goinsta/v3" 11 | ) 12 | 13 | func TestTimeline(t *testing.T) { 14 | insta, err := goinsta.EnvRandAcc() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | t.Logf("Logged in as %s\n", insta.Account.Username) 19 | 20 | tl := insta.Timeline 21 | next := tl.NextID 22 | outside: 23 | for i := 0; tl.Next(); i++ { 24 | if tl.NextID == next { 25 | t.Fatal("Next ID must be different after each request") 26 | } 27 | next = tl.NextID 28 | if i == 5 { 29 | break outside 30 | } 31 | time.Sleep(time.Duration(rand.Intn(10)) * time.Second) 32 | } 33 | 34 | t.Logf("Gathered %d posts, %f on last request\n", len(tl.Items), tl.NumResults) 35 | } 36 | 37 | func TestDownload(t *testing.T) { 38 | insta, err := goinsta.EnvRandAcc() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | t.Logf("Logged in as %s\n", insta.Account.Username) 43 | 44 | if !insta.Timeline.Next() { 45 | t.Fatal(insta.Timeline.Error()) 46 | } 47 | posts := insta.Timeline.Items 48 | if len(posts) == 0 { 49 | t.Fatal("No posts found") 50 | } 51 | 52 | rand.Seed(time.Now().UnixNano()) 53 | randN := rand.Intn(len(posts)) 54 | post := posts[randN] 55 | 56 | folder := "downloads/" + strconv.FormatInt(time.Now().Unix(), 10) 57 | err = post.DownloadTo(path.Join(folder, "")) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | randN = rand.Intn(len(posts)) 63 | post = posts[randN] 64 | err = post.DownloadTo(path.Join(folder, "testy")) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | t.Logf("Downloaded posts to %s", folder) 69 | 70 | err = post.User.DownloadProfilePicTo(path.Join(folder, "profilepic")) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/upload_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | "testing" 10 | 11 | "github.com/Davincible/goinsta/v3" 12 | ) 13 | 14 | func TestUploadPhoto(t *testing.T) { 15 | insta, err := goinsta.EnvRandAcc() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | t.Logf("Logged in as %s\n", insta.Account.Username) 20 | insta.SetWarnHandler(t.Log) 21 | 22 | // Get random photo 23 | photo, err := getPhoto(1400, 1400) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | results, err := insta.Searchbar.SearchLocation("New York") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if len(results.Places) == 0 { 33 | t.Fatal(errors.New("No search result found")) 34 | } 35 | location := results.Places[0].Location 36 | if err := results.RegisterLocationClick(location); err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | item, err := insta.Upload( 41 | &goinsta.UploadOptions{ 42 | File: photo, 43 | Caption: "awesome! :) #41", 44 | Location: location.NewPostTag(), 45 | UserTags: &[]goinsta.UserTag{ 46 | { 47 | User: &goinsta.User{ 48 | ID: insta.Account.ID, 49 | }, 50 | }, 51 | }, 52 | }, 53 | ) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | t.Logf("The ID of the new upload is %s", item.ID) 58 | } 59 | 60 | func TestUploadThumbVideo(t *testing.T) { 61 | insta, err := goinsta.EnvRandAcc() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | t.Logf("Logged in as %s\n", insta.Account.Username) 66 | insta.SetWarnHandler(t.Log) 67 | 68 | // Get random video 69 | video, err := getVideo() 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | size := float64(len(video.Content)) / 1000000.0 74 | t.Logf("Video size: %.2f Mb", size) 75 | 76 | photo, err := getPhoto(1920, 1080) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | // Find location 82 | results, err := insta.Searchbar.SearchLocation("Chicago") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | if len(results.Places) == 0 { 87 | t.Fatal(errors.New("No search result found")) 88 | } 89 | 90 | location := results.Places[1].Location 91 | if err := results.RegisterLocationClick(location); err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | item, err := insta.Upload( 96 | &goinsta.UploadOptions{ 97 | File: bytes.NewReader(video.Content), 98 | Thumbnail: photo, 99 | Caption: "What a terrific video! #art", 100 | Location: location.NewPostTag(), 101 | UserTags: &[]goinsta.UserTag{ 102 | { 103 | User: &goinsta.User{ 104 | ID: insta.Account.ID, 105 | }, 106 | }, 107 | }, 108 | }, 109 | ) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | t.Logf("The ID of the new upload is %s", item.ID) 114 | } 115 | 116 | func TestUploadVideo(t *testing.T) { 117 | insta, err := goinsta.EnvRandAcc() 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | t.Logf("Logged in as %s\n", insta.Account.Username) 122 | insta.SetWarnHandler(t.Log) 123 | 124 | // Get random video 125 | video, err := getVideo() 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | size := float64(len(video.Content)) / 1000000.0 130 | t.Logf("Video size: %.2f Mb", size) 131 | 132 | // Find location 133 | results, err := insta.Searchbar.SearchLocation("Bali") 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if len(results.Places) == 0 { 138 | t.Fatal(errors.New("No search result found")) 139 | } 140 | location := results.Places[1].Location 141 | if err := results.RegisterLocationClick(location); err != nil { 142 | t.Fatal(err) 143 | } 144 | 145 | item, err := insta.Upload( 146 | &goinsta.UploadOptions{ 147 | File: bytes.NewReader(video.Content), 148 | Caption: "What a terrific video! #art", 149 | Location: location.NewPostTag(), 150 | UserTags: &[]goinsta.UserTag{ 151 | { 152 | User: &goinsta.User{ 153 | ID: insta.Account.ID, 154 | }, 155 | }, 156 | }, 157 | }, 158 | ) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | t.Logf("The ID of the new upload is %s", item.ID) 163 | } 164 | 165 | func TestUploadStoryPhoto(t *testing.T) { 166 | insta, err := goinsta.EnvRandAcc() 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | t.Logf("Logged in as %s\n", insta.Account.Username) 171 | insta.SetWarnHandler(t.Log) 172 | 173 | // Get random photo 174 | photo, err := getPhoto(1400, 1400) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | 179 | item, err := insta.Upload( 180 | &goinsta.UploadOptions{ 181 | File: photo, 182 | IsStory: true, 183 | }, 184 | ) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | t.Logf("The ID of the new upload is %s", item.ID) 189 | } 190 | 191 | func TestUploadStoryVideo(t *testing.T) { 192 | insta, err := goinsta.EnvRandAcc() 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | t.Logf("Logged in as %s\n", insta.Account.Username) 197 | insta.SetWarnHandler(t.Log) 198 | 199 | // Get random video 200 | video, err := getVideo(map[string]interface{}{"max_length": 20}) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | size := float64(len(video.Content)) / 1000000.0 205 | t.Logf("Video size: %.2f Mb", size) 206 | 207 | item, err := insta.Upload( 208 | &goinsta.UploadOptions{ 209 | File: bytes.NewReader(video.Content), 210 | IsStory: true, 211 | }, 212 | ) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | t.Logf("The ID of the new upload is %s", item.ID) 217 | } 218 | 219 | func TestUploadStoryMultiVideo(t *testing.T) { 220 | insta, err := goinsta.EnvRandAcc() 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | t.Logf("Logged in as %s\n", insta.Account.Username) 225 | insta.SetWarnHandler(t.Log) 226 | 227 | // Get random videos 228 | album := []io.Reader{} 229 | for i := 0; i < 5; i++ { 230 | video, err := getVideo(map[string]interface{}{"max_length": 20}) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | size := float64(len(video.Content)) / 1000000.0 235 | t.Logf("Video size: %.2f Mb", size) 236 | album = append(album, bytes.NewReader(video.Content)) 237 | } 238 | 239 | item, err := insta.Upload( 240 | &goinsta.UploadOptions{ 241 | Album: album, 242 | IsStory: true, 243 | }, 244 | ) 245 | if err != nil { 246 | t.Fatal(err) 247 | } 248 | t.Logf("The ID of the last story is %s", item.ID) 249 | } 250 | 251 | func TestUploadCarousel(t *testing.T) { 252 | insta, err := goinsta.EnvRandAcc() 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | t.Logf("Logged in as %s\n", insta.Account.Username) 257 | insta.SetWarnHandler(t.Log) 258 | 259 | // Get random photos 260 | album := []io.Reader{} 261 | for i := 0; i < 5; i++ { 262 | photo, err := getPhoto(1920, 1080) 263 | if err != nil { 264 | log.Fatal(err) 265 | } 266 | 267 | album = append(album, photo) 268 | } 269 | 270 | // Add video to album 271 | video, err := getVideo() 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | size := float64(len(video.Content)) / 1000000.0 276 | t.Logf("Video size: %.2f Mb", size) 277 | album = append(album, bytes.NewReader(video.Content)) 278 | 279 | results, err := insta.Searchbar.SearchLocation("New York") 280 | if err != nil { 281 | t.Fatal(err) 282 | } 283 | if len(results.Places) == 0 { 284 | t.Fatal(errors.New("No search result found")) 285 | } 286 | location := results.Places[1].Location 287 | if err := results.RegisterLocationClick(location); err != nil { 288 | t.Fatal(err) 289 | } 290 | 291 | // Upload Album 292 | item, err := insta.Upload( 293 | &goinsta.UploadOptions{ 294 | Album: album, 295 | Location: location.NewPostTag(), 296 | Caption: "The best photos I've seen all morning!", 297 | UserTags: &[]goinsta.UserTag{ 298 | { 299 | User: &goinsta.User{ 300 | ID: insta.Account.ID, 301 | }, 302 | }, 303 | }, 304 | }, 305 | ) 306 | if err != nil { 307 | t.Fatal(err) 308 | } 309 | t.Logf("The ID of the new upload is %s", item.ID) 310 | } 311 | 312 | func TestUploadProfilePicture(t *testing.T) { 313 | insta, err := goinsta.EnvRandAcc() 314 | if err != nil { 315 | t.Fatal(err) 316 | } 317 | t.Logf("Logged in as %s\n", insta.Account.Username) 318 | insta.SetWarnHandler(t.Log) 319 | 320 | file := "./downloads/1645304867/testy_1.jpg" 321 | b, err := os.ReadFile(file) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | 326 | buf := bytes.NewBuffer(b) 327 | if err := insta.Account.ChangeProfilePic(buf); err != nil { 328 | t.Fatal(err) 329 | } 330 | t.Log("Changed profile picture!") 331 | } 332 | -------------------------------------------------------------------------------- /timeline.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "math/rand" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type ( 12 | fetchReason string 13 | ) 14 | 15 | var ( 16 | PULLTOREFRESH fetchReason = "pull_to_refresh" 17 | COLDSTART fetchReason = "cold_start_fetch" 18 | WARMSTART fetchReason = "warm_start_fetch" 19 | PAGINATION fetchReason = "pagination" 20 | AUTOREFRESH fetchReason = "auto_refresh" // so far unused 21 | ) 22 | 23 | // Timeline is the object to represent the main feed on instagram, the first page that shows the latest feeds of my following contacts. 24 | type Timeline struct { 25 | insta *Instagram 26 | err error 27 | lastRequest int64 28 | pullRefresh bool 29 | sessionID string 30 | prevReason fetchReason 31 | fetchExtra bool 32 | 33 | endpoint string 34 | Items []*Item 35 | Tray *Tray 36 | 37 | MoreAvailable bool 38 | NextID string 39 | NumResults float64 40 | PreloadDistance float64 41 | PullToRefreshWindowMs float64 42 | RequestID string 43 | SessionID string 44 | } 45 | 46 | type feedCache struct { 47 | Items []struct { 48 | MediaOrAd *Item `json:"media_or_ad"` 49 | EndOfFeed struct { 50 | Pause bool `json:"pause"` 51 | Title string `json:"title"` 52 | Subtitle string `json:"subtitle"` 53 | } `json:"end_of_feed_demarcator"` 54 | } `json:"feed_items"` 55 | 56 | MoreAvailable bool `json:"more_available"` 57 | NextID string `json:"next_max_id"` 58 | NumResults float64 `json:"num_results"` 59 | PullToRefreshWindowMs float64 `json:"pull_to_refresh_window_ms"` 60 | RequestID string `json:"request_id"` 61 | SessionID string `json:"session_id"` 62 | ViewStateVersion string `json:"view_state_version"` 63 | AutoLoadMore bool `json:"auto_load_more_enabled"` 64 | IsDirectV2Enabled bool `json:"is_direct_v2_enabled"` 65 | ClientFeedChangelistApplied bool `json:"client_feed_changelist_applied"` 66 | PreloadDistance float64 `json:"preload_distance"` 67 | Status string `json:"status"` 68 | FeedPillText string `json:"feed_pill_text"` 69 | StartupPrefetchConfigs struct { 70 | Explore struct { 71 | ContainerModule string `json:"containermodule"` 72 | ShouldPrefetch bool `json:"should_prefetch"` 73 | ShouldPrefetchThumbnails bool `json:"should_prefetch_thumbnails"` 74 | } `json:"explore"` 75 | } `json:"startup_prefetch_configs"` 76 | UseAggressiveFirstTailLoad bool `json:"use_aggressive_first_tail_load"` 77 | HideLikeAndViewCounts float64 `json:"hide_like_and_view_counts"` 78 | } 79 | 80 | func newTimeline(insta *Instagram) *Timeline { 81 | time := &Timeline{ 82 | insta: insta, 83 | endpoint: urlTimeline, 84 | } 85 | return time 86 | } 87 | 88 | // Next allows pagination after calling: 89 | // User.Feed 90 | // returns false when list reach the end. 91 | // if Timeline.Error() is ErrNoMore no problem have been occurred. 92 | // starts first request will be a cold start 93 | func (tl *Timeline) Next(p ...interface{}) bool { 94 | if tl.err != nil { 95 | return false 96 | } 97 | 98 | insta := tl.insta 99 | endpoint := tl.endpoint 100 | 101 | // make sure at least 4 sec after last request, at most 6 sec 102 | var th int64 = 4 103 | var thR float64 = 2 104 | 105 | // if fetching extra, no big timeout is needed 106 | if tl.fetchExtra { 107 | th = 2 108 | thR = 1 109 | } 110 | 111 | if delta := time.Now().Unix() - tl.lastRequest; delta < th { 112 | s := time.Duration(rand.Float64()*thR + float64(th-delta)) 113 | time.Sleep(s * time.Second) 114 | } 115 | t := time.Now().Unix() 116 | 117 | var reason fetchReason 118 | isPullToRefresh := "0" 119 | query := map[string]string{ 120 | "feed_view_info": "[]", 121 | "timezone_offset": timeOffset, 122 | "device_id": insta.uuid, 123 | "request_id": generateUUID(), 124 | "_uuid": insta.uuid, 125 | "bloks_versioning_id": bloksVerID, 126 | } 127 | 128 | var tWarm int64 = 10 129 | if tl.pullRefresh || (!tl.MoreAvailable && t-tl.lastRequest < tWarm*60) { 130 | reason = PULLTOREFRESH 131 | isPullToRefresh = "1" 132 | } else if tl.lastRequest == 0 || (tl.fetchExtra && tl.prevReason == "warm_start_fetch") { 133 | reason = COLDSTART 134 | } else if t-tl.lastRequest > tWarm*60 { // 10 min 135 | reason = WARMSTART 136 | } else if tl.fetchExtra || tl.MoreAvailable && tl.NextID != "" { 137 | reason = PAGINATION 138 | query["max_id"] = tl.NextID 139 | } 140 | 141 | wg := &sync.WaitGroup{} 142 | defer wg.Wait() 143 | errChan := make(chan error) 144 | 145 | if reason != PAGINATION { 146 | tl.sessionID = generateUUID() 147 | 148 | wg.Add(1) 149 | go func() { 150 | defer wg.Done() 151 | err := tl.FetchTray(reason) 152 | if err != nil { 153 | errChan <- err 154 | } 155 | }() 156 | } 157 | 158 | query["reason"] = string(reason) 159 | query["is_pull_to_refresh"] = isPullToRefresh 160 | query["session_id"] = tl.sessionID 161 | tl.prevReason = reason 162 | 163 | body, _, err := insta.sendRequest( 164 | &reqOptions{ 165 | Endpoint: endpoint, 166 | IsPost: true, 167 | Gzip: true, 168 | Query: query, 169 | ExtraHeaders: map[string]string{ 170 | "X-Ads-Opt-Out": "0", 171 | "X-Google-AD-ID": insta.adid, 172 | "X-Fb": "1", 173 | }, 174 | }, 175 | ) 176 | if err != nil { 177 | tl.err = err 178 | return false 179 | } 180 | 181 | tl.lastRequest = t 182 | 183 | // Decode json 184 | tmp := feedCache{} 185 | d := json.NewDecoder(bytes.NewReader(body)) 186 | d.UseNumber() 187 | err = d.Decode(&tmp) 188 | 189 | // Add posts to Timeline object 190 | if err != nil { 191 | tl.err = err 192 | return false 193 | } 194 | 195 | // copy constants over 196 | tl.NextID = tmp.NextID 197 | tl.MoreAvailable = tmp.MoreAvailable 198 | if tl.fetchExtra { 199 | tl.NumResults += tmp.NumResults 200 | } else { 201 | tl.NumResults = tmp.NumResults 202 | } 203 | tl.PreloadDistance = tmp.PreloadDistance 204 | tl.PullToRefreshWindowMs = tmp.PullToRefreshWindowMs 205 | tl.fetchExtra = false 206 | 207 | // copy post items over 208 | for _, i := range tmp.Items { 209 | // will be nil if end of feed, EndOfFeed will then be set 210 | if i.MediaOrAd != nil { 211 | setToItem(i.MediaOrAd, tl) 212 | tl.Items = append(tl.Items, i.MediaOrAd) 213 | } 214 | } 215 | 216 | // Set index value 217 | for i, v := range tl.Items { 218 | v.Index = i 219 | } 220 | 221 | // fetch more posts if not enough posts were returned, mimick apk behvaior 222 | if reason != PULLTOREFRESH && tmp.NumResults < tmp.PreloadDistance && tmp.MoreAvailable { 223 | tl.fetchExtra = true 224 | tl.Next() 225 | } 226 | 227 | // Check if stories returned an error 228 | select { 229 | case err := <-errChan: 230 | if err != nil { 231 | tl.err = err 232 | return false 233 | } 234 | default: 235 | } 236 | 237 | if !tl.MoreAvailable { 238 | tl.err = ErrNoMore 239 | return false 240 | } 241 | 242 | return true 243 | } 244 | 245 | // SetPullRefresh will set a flag to refresh the timeline on subsequent .Next() call 246 | func (tl *Timeline) SetPullRefresh() { 247 | tl.pullRefresh = true 248 | } 249 | 250 | // UnsetPullRefresh will unset the pull to refresh flag, if you previously manually 251 | // set it, and want to unset it. 252 | func (tl *Timeline) UnsetPullRefresh() { 253 | tl.pullRefresh = false 254 | } 255 | 256 | // ClearPosts will unreference the current list of post items. Used when calling 257 | // .Refresh() 258 | func (tl *Timeline) ClearPosts() { 259 | tl.Items = []*Item{} 260 | tl.Tray = &Tray{} 261 | } 262 | 263 | // FetchTray fetches the timeline tray with story media. 264 | // This function should rarely be called manually. If you want to refresh 265 | // the timeline call Timeline.Refresh() 266 | func (tl *Timeline) FetchTray(r fetchReason) error { 267 | insta := tl.insta 268 | 269 | var reason string 270 | switch r { 271 | case PULLTOREFRESH: 272 | reason = string(PULLTOREFRESH) 273 | case COLDSTART: 274 | reason = "cold_start" 275 | case WARMSTART: 276 | reason = "warm_start_with_feed" 277 | } 278 | 279 | body, _, err := insta.sendRequest( 280 | &reqOptions{ 281 | Endpoint: urlStories, 282 | IsPost: true, 283 | Query: map[string]string{ 284 | "supported_capabilities_new": `[{"name":"SUPPORTED_SDK_VERSIONS","value":"100.0,101.0,102.0,103.0,104.0,105.0,106.0,107.0,108.0,109.0,110.0,111.0,112.0,113.0,114.0,115.0,116.0,117.0"},{"name":"FACE_TRACKER_VERSION","value":"14"},{"name":"segmentation","value":"segmentation_enabled"},{"name":"COMPRESSION","value":"ETC2_COMPRESSION"},{"name":"world_tracker","value":"world_tracker_enabled"},{"name":"gyroscope","value":"gyroscope_enabled"}]`, 285 | "reason": reason, 286 | "timezone_offset": timeOffset, 287 | "tray_session_id": generateUUID(), 288 | "request_id": generateUUID(), 289 | "_uuid": tl.insta.uuid, 290 | }, 291 | }, 292 | ) 293 | if err != nil { 294 | return err 295 | } 296 | 297 | tray := &Tray{} 298 | err = json.Unmarshal(body, tray) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | tray.set(tl.insta) 304 | tl.Tray = tray 305 | return nil 306 | } 307 | 308 | // Refresh will clear the current list of posts, perform a pull to refresh action, 309 | // and refresh the current timeline. 310 | func (tl *Timeline) Refresh() error { 311 | tl.ClearPosts() 312 | tl.SetPullRefresh() 313 | if !tl.Next() { 314 | return tl.err 315 | } 316 | return nil 317 | } 318 | 319 | // NewFeedPostsExist will return true if new feed posts are available. 320 | func (tl *Timeline) NewFeedPostsExist() (bool, error) { 321 | insta := tl.insta 322 | 323 | body, err := insta.sendSimpleRequest(urlFeedNewPostsExist) 324 | if err != nil { 325 | return false, err 326 | } 327 | 328 | var resp struct { 329 | NewPosts bool `json:"new_feed_posts_exist"` 330 | Status string `json:"status"` 331 | } 332 | err = json.Unmarshal(body, &resp) 333 | if err != nil { 334 | return false, err 335 | } 336 | return resp.NewPosts, nil 337 | } 338 | 339 | // Stories is a helper function to get the stories 340 | func (tl *Timeline) Stories() []*Reel { 341 | return tl.Tray.Stories 342 | } 343 | 344 | // helper function to get the Broadcasts 345 | func (tl *Timeline) Broadcasts() []*Broadcast { 346 | return tl.Tray.Broadcasts 347 | } 348 | 349 | func (tl *Timeline) GetNextID() string { 350 | return tl.NextID 351 | } 352 | 353 | // Delete is only a placeholder, it does nothing 354 | func (tl *Timeline) Delete() error { 355 | return nil 356 | } 357 | 358 | func (tl *Timeline) getInsta() *Instagram { 359 | return tl.insta 360 | } 361 | 362 | // Error will the error of the Timeline instance if one occured 363 | func (tl *Timeline) Error() error { 364 | return tl.err 365 | } 366 | -------------------------------------------------------------------------------- /twofactor.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/Davincible/goinsta/v3/utilities" 8 | ) 9 | 10 | type TwoFactorInfo struct { 11 | insta *Instagram 12 | 13 | ID int64 `json:"pk"` 14 | Username string `json:"username"` 15 | 16 | ElegibleForMultipleTotp bool `json:"elegible_for_multiple_totp"` 17 | ObfuscatedPhoneNr string `json:"obfuscated_phone_number"` 18 | PendingTrustedNotification bool `json:"pending_trusted_notification"` 19 | ShouldOptInTrustedDevice bool `json:"should_opt_in_trusted_device_option"` 20 | ShowMessengerCodeOption bool `json:"show_messenger_code_option"` 21 | ShowTrustedDeviceOption bool `json:"show_trusted_device_option"` 22 | SMSNotAllowedReason string `json:"sms_not_allowed_reason"` 23 | SMSTwoFactorOn bool `json:"sms_two_factor_on"` 24 | TotpTwoFactorOn bool `json:"totp_two_factor_on"` 25 | WhatsappTwoFactorOn bool `json:"whatsapp_two_factor_on"` 26 | TwoFactorIdentifier string `json:"two_factor_identifier"` 27 | 28 | PhoneVerificationSettings phoneVerificationSettings `json:"phone_verification_settings"` 29 | } 30 | 31 | type phoneVerificationSettings struct { 32 | MaxSMSCount int `json:"max_sms_count"` 33 | ResendSMSDelaySec int `json:"resend_sms_delay_sec"` 34 | RobocallAfterMaxSms bool `json:"robocall_after_max_sms"` 35 | RobocallCountDownSec int `json:"robocall_count_down_time_sec"` 36 | } 37 | 38 | // Login2FA allows for a login through 2FA 39 | // You can either provide a code directly by passing it as a parameter, or 40 | // goinsta can generate one for you as long as the TOTP seed is set. 41 | func (info *TwoFactorInfo) Login2FA(in ...string) error { 42 | insta := info.insta 43 | 44 | var code string 45 | if len(in) > 0 { 46 | code = in[0] 47 | } else if info.insta.totp == nil || info.insta.totp.Seed == "" { 48 | return Err2FANoCode 49 | } else { 50 | otp, err := utilities.GenTOTP(insta.totp.Seed) 51 | if err != nil { 52 | return fmt.Errorf("Failed to generate 2FA OTP code: %w", err) 53 | } 54 | code = otp 55 | } 56 | 57 | data, err := json.Marshal( 58 | map[string]string{ 59 | "verification_code": code, 60 | "phone_id": insta.fID, 61 | "two_factor_identifier": info.TwoFactorIdentifier, 62 | "username": insta.user, 63 | "trust_this_device": "1", 64 | "guid": insta.uuid, 65 | "device_id": insta.dID, 66 | "waterfall_id": generateUUID(), 67 | "verification_method": "3", 68 | }, 69 | ) 70 | if err != nil { 71 | return err 72 | } 73 | body, _, err := insta.sendRequest( 74 | &reqOptions{ 75 | Endpoint: url2FALogin, 76 | IsPost: true, 77 | Query: generateSignature(data), 78 | IgnoreHeaders: []string{ 79 | "Ig-U-Shbts", 80 | "Ig-U-Shbid", 81 | "Ig-U-Rur", 82 | "Authorization", 83 | }, 84 | ExtraHeaders: map[string]string{ 85 | "X-Ig-Www-Claim": "0", 86 | "Ig-Intended-User-Id": "0", 87 | }, 88 | }, 89 | ) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | err = insta.parseLogin(body) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = insta.OpenApp() 100 | return err 101 | } 102 | 103 | // Check2FATrusted checks whether the device has been trusted. 104 | // When you enable 2FA, you can verify, or trust, the device with one of your 105 | // other devices. This method will check if this device has been trusted. 106 | // if so, it will login, if not, it will return an error. 107 | // The android app calls this method every 3 seconds 108 | func (info *TwoFactorInfo) Check2FATrusted() error { 109 | insta := info.insta 110 | body, _, err := insta.sendRequest( 111 | &reqOptions{ 112 | Endpoint: url2FACheckTrusted, 113 | Query: map[string]string{ 114 | "two_factor_identifier": info.TwoFactorIdentifier, 115 | "username": insta.user, 116 | "device_id": insta.dID, 117 | }, 118 | }, 119 | ) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | var stat struct { 125 | ReviewStatus int `json:"review_status"` 126 | Status string `json:"status"` 127 | } 128 | if err = json.Unmarshal(body, &stat); err != nil { 129 | return err 130 | } 131 | 132 | if stat.ReviewStatus == 0 { 133 | return fmt.Errorf("two factor authentication not yet verified") 134 | } 135 | 136 | err = info.Login2FA("") 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /utilities/abool.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | import "sync" 4 | 5 | type ABool struct { 6 | value bool 7 | mu *sync.RWMutex 8 | } 9 | 10 | func (b *ABool) Set(val bool) { 11 | b.mu.Lock() 12 | b.value = val 13 | b.mu.Unlock() 14 | } 15 | 16 | func (b *ABool) Get() bool { 17 | b.mu.RLock() 18 | defer b.mu.RUnlock() 19 | return b.value 20 | } 21 | 22 | func NewABool() *ABool { 23 | return &ABool{ 24 | mu: &sync.RWMutex{}, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utilities/encryption.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/binary" 11 | "encoding/pem" 12 | "fmt" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | func RSADecodePublicKeyFromBase64(pubKeyBase64 string) (*rsa.PublicKey, error) { 18 | pubKey, err := base64.StdEncoding.DecodeString(pubKeyBase64) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | block, _ := pem.Decode(pubKey) 24 | pKey, err := x509.ParsePKIXPublicKey(block.Bytes) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return pKey.(*rsa.PublicKey), nil 29 | } 30 | 31 | func AESGCMEncrypt(key, data, additionalData []byte) (iv, encrypted, tag []byte, err error) { 32 | iv = make([]byte, 12) 33 | if _, err = rand.Read(iv); err != nil { 34 | return 35 | } 36 | 37 | var block cipher.Block 38 | block, err = aes.NewCipher(key) 39 | if err != nil { 40 | err = fmt.Errorf("error when creating cipher: %w", err) 41 | return 42 | } 43 | 44 | var aesgcm cipher.AEAD 45 | aesgcm, err = cipher.NewGCM(block) 46 | if err != nil { 47 | err = fmt.Errorf("error when creating gcm: %w", err) 48 | return 49 | } 50 | 51 | encrypted = aesgcm.Seal(nil, iv, data, additionalData) 52 | tag = encrypted[len(encrypted)-16:] // Extracting last 16 bytes authentication tag 53 | encrypted = encrypted[:len(encrypted)-16] // Extracting raw Encrypted data without IV & Tag for use in NodeJS 54 | 55 | return 56 | } 57 | 58 | func RSAPublicKeyPKCS1Encrypt(publicKey *rsa.PublicKey, data []byte) ([]byte, error) { 59 | return rsa.EncryptPKCS1v15(rand.Reader, publicKey, data) 60 | } 61 | 62 | func EncryptPassword(password, pubKeyEncoded string, pubKeyVersion int, t string) (string, error) { 63 | if t == "" { 64 | t = strconv.FormatInt(time.Now().Unix(), 10) 65 | } 66 | 67 | // Get the public key 68 | publicKey, err := RSADecodePublicKeyFromBase64(pubKeyEncoded) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | // Data to be encrypted by RSA PKCS1 74 | randKey := make([]byte, 32) 75 | if _, err := rand.Read(randKey); err != nil { 76 | return "", err 77 | } 78 | 79 | // Encrypt the random key that will be used to encrypt the password 80 | randKeyEncrypted, err := RSAPublicKeyPKCS1Encrypt(publicKey, randKey) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | // Get the size of the encrypted random key 86 | randKeyEncryptedSize := make([]byte, 2) 87 | binary.LittleEndian.PutUint16(randKeyEncryptedSize[:], uint16(len(randKeyEncrypted))) 88 | 89 | // Encrypt the password using AES GCM with the random key 90 | iv, encrypted, tag, err := AESGCMEncrypt(randKey, []byte(password), []byte(t)) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | // Combine the parts 96 | s := []byte{} 97 | prefix := []byte{1, byte(pubKeyVersion)} 98 | parts := [][]byte{prefix, iv, randKeyEncryptedSize, randKeyEncrypted, tag, encrypted} 99 | for _, b := range parts { 100 | s = append(s, b...) 101 | } 102 | 103 | encoded := base64.StdEncoding.EncodeToString(s) 104 | r := fmt.Sprintf("#PWD_INSTAGRAM:4:%s:%s", t, encoded) 105 | 106 | return r, nil 107 | } 108 | -------------------------------------------------------------------------------- /utilities/totp.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "encoding/base32" 8 | "encoding/binary" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // prefix will append extra 0s if the length of otp is less than 6. 15 | func prefix(otp string) string { 16 | if len(otp) == 6 { 17 | return otp 18 | } 19 | for i := (6 - len(otp)); i > 0; i-- { 20 | otp = "0" + otp 21 | } 22 | return otp 23 | } 24 | 25 | // genHOTP will generate a Hmac One Time Password 26 | func genHOTP(secret string, interval int64) (string, error) { 27 | // Decode base32 secret 28 | key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret)) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | bs := make([]byte, 8) 34 | binary.BigEndian.PutUint64(bs, uint64(interval)) 35 | 36 | // Signing the value using HMAC-SHA1 Algorithm 37 | hash := hmac.New(sha1.New, key) 38 | hash.Write(bs) 39 | h := hash.Sum(nil) 40 | 41 | // Grab offset 42 | o := (h[19] & 15) 43 | 44 | // Get 32 bit chunk from hash starting at the o 45 | var header uint32 46 | r := bytes.NewReader(h[o : o+4]) 47 | err = binary.Read(r, binary.BigEndian, &header) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | // Ignore most significant bits as per RFC 4226, and crop to 6 digits. 53 | h12 := (int(header) & 0x7fffffff) % 1000000 54 | 55 | otp := strconv.Itoa(h12) 56 | otp = prefix(otp) 57 | 58 | return otp, nil 59 | } 60 | 61 | // GenTOTP will generate a one time pass based on the secret. 62 | func GenTOTP(secret string) (string, error) { 63 | //The TOTP token is just a HOTP token seeded with every 30 seconds. 64 | interval := time.Now().Unix() / 30 65 | otp, err := genHOTP(secret, interval) 66 | return otp, err 67 | } 68 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "encoding/json" 8 | "image" 9 | "math" 10 | "regexp" 11 | 12 | // Required for getImageDimensionFromReader in jpg and png format 13 | "fmt" 14 | _ "image/jpeg" 15 | _ "image/png" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | func formatID(id interface{}) string { 21 | switch s := id.(type) { 22 | case string: 23 | return s 24 | case int64: 25 | return strconv.FormatInt(s, 10) 26 | case json.Number: 27 | return string(s) 28 | } 29 | return "" 30 | } 31 | 32 | func toString(i interface{}) string { 33 | switch s := i.(type) { 34 | case string: 35 | return s 36 | case bool: 37 | return strconv.FormatBool(s) 38 | case float64: 39 | return strconv.FormatFloat(s, 'f', -1, 64) 40 | case float32: 41 | return strconv.FormatFloat(float64(s), 'f', -1, 32) 42 | case int: 43 | return strconv.Itoa(s) 44 | case int64: 45 | return strconv.FormatInt(s, 10) 46 | case int32: 47 | return strconv.Itoa(int(s)) 48 | case int16: 49 | return strconv.FormatInt(int64(s), 10) 50 | case int8: 51 | return strconv.FormatInt(int64(s), 10) 52 | case uint: 53 | return strconv.FormatInt(int64(s), 10) 54 | case uint64: 55 | return strconv.FormatInt(int64(s), 10) 56 | case uint32: 57 | return strconv.FormatInt(int64(s), 10) 58 | case uint16: 59 | return strconv.FormatInt(int64(s), 10) 60 | case uint8: 61 | return strconv.FormatInt(int64(s), 10) 62 | case []byte: 63 | return string(s) 64 | case error: 65 | return s.Error() 66 | } 67 | return "" 68 | } 69 | 70 | func prepareRecipients(cc interface{}) (bb string, err error) { 71 | var b []byte 72 | ids := make([][]int64, 0) 73 | switch c := cc.(type) { 74 | case *Conversation: 75 | for i := range c.Users { 76 | ids = append(ids, []int64{c.Users[i].ID}) 77 | } 78 | case *Item: 79 | ids = append(ids, []int64{c.User.ID}) 80 | case int64: 81 | ids = append(ids, []int64{c}) 82 | } 83 | b, err = json.Marshal(ids) 84 | bb = string(b) 85 | return 86 | } 87 | 88 | // getImageSize return image dimension , types is .jpg and .png 89 | func getImageSize(b []byte) (int, int, error) { 90 | buf := bytes.NewReader(b) 91 | image, _, err := image.DecodeConfig(buf) 92 | if err != nil { 93 | return 0, 0, err 94 | } 95 | return image.Width, image.Height, nil 96 | } 97 | 98 | func getVideoInfo(b []byte) (height, width, duration int, err error) { 99 | keys := []string{"moov", "trak", "stbl", "avc1"} 100 | height, err = read16(b, keys, 24) 101 | if err != nil { 102 | return 103 | } 104 | width, err = read16(b, keys, 26) 105 | if err != nil { 106 | return 107 | } 108 | 109 | duration, err = getMP4Duration(b) 110 | if err != nil { 111 | return 112 | } 113 | 114 | return 115 | } 116 | 117 | func getMP4Duration(b []byte) (int, error) { 118 | keys := []string{"moov", "mvhd"} 119 | timescale, err := read32(b, keys, 12) 120 | if err != nil { 121 | return -1, err 122 | } 123 | 124 | // If timescale failed to read a value, return -1 * 1000 125 | if timescale == 0 { 126 | return -1000, nil 127 | } 128 | 129 | length, err := read32(b, keys, 12+4) 130 | if err != nil { 131 | return -1, err 132 | } 133 | 134 | return int(math.Floor(float64(length) / float64(timescale) * 1000)), nil 135 | } 136 | 137 | func getTimeOffset() string { 138 | _, offset := time.Now().Zone() 139 | return strconv.Itoa(offset) 140 | } 141 | 142 | func jazoest(str string) string { 143 | b := []byte(str) 144 | var s int 145 | for v := range b { 146 | s += v 147 | } 148 | return "2" + strconv.Itoa(s) 149 | } 150 | 151 | func createUserAgent(device Device) string { 152 | // Instagram 195.0.0.31.123 Android (28/9; 560dpi; 1440x2698; LGE/lge; LG-H870DS; lucye; lucye; en_GB; 302733750) 153 | // Instagram 195.0.0.31.123 Android (28/9; 560dpi; 1440x2872; Genymotion/Android; Samsung Galaxy S10; vbox86p; vbox86; en_US; 302733773) # version_code: 302733773 154 | // Instagram 195.0.0.31.123 Android (30/11; 560dpi; 1440x2898; samsung; SM-G975F; beyond2; exynos9820; en_US; 302733750) 155 | return fmt.Sprintf("Instagram %s Android (%d/%d; %s; %s; %s; %s; %s; %s; %s; %s)", 156 | appVersion, 157 | device.AndroidVersion, 158 | device.AndroidRelease, 159 | device.ScreenDpi, 160 | device.ScreenResolution, 161 | device.Manufacturer, 162 | device.Model, 163 | device.CodeName, 164 | device.Chipset, 165 | locale, 166 | appVersionCode, 167 | ) 168 | } 169 | 170 | // ExportAsBytes exports selected *Instagram object as []byte 171 | func (insta *Instagram) ExportAsBytes() ([]byte, error) { 172 | buffer := &bytes.Buffer{} 173 | err := insta.ExportIO(buffer) 174 | if err != nil { 175 | return nil, err 176 | } 177 | return buffer.Bytes(), nil 178 | } 179 | 180 | // ExportAsBase64String exports selected *Instagram object as base64 encoded string 181 | func (insta *Instagram) ExportAsBase64String() (string, error) { 182 | bytes, err := insta.ExportAsBytes() 183 | if err != nil { 184 | return "", err 185 | } 186 | 187 | sEnc := base64.StdEncoding.EncodeToString(bytes) 188 | return sEnc, nil 189 | } 190 | 191 | // ImportFromBytes imports instagram configuration from an array of bytes. 192 | // 193 | // This function does not set proxy automatically. Use SetProxy after this call. 194 | func ImportFromBytes(inputBytes []byte, args ...interface{}) (*Instagram, error) { 195 | return ImportReader(bytes.NewReader(inputBytes), args...) 196 | } 197 | 198 | // ImportFromBase64String imports instagram configuration from a base64 encoded string. 199 | // 200 | // This function does not set proxy automatically. Use SetProxy after this call. 201 | func ImportFromBase64String(base64String string, args ...interface{}) (*Instagram, error) { 202 | sDec, err := base64.StdEncoding.DecodeString(base64String) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | return ImportFromBytes(sDec, args...) 208 | } 209 | 210 | func MergeMapI(one map[string]interface{}, extra ...map[string]interface{}) map[string]interface{} { 211 | for _, e := range extra { 212 | for k, v := range e { 213 | one[k] = v 214 | } 215 | } 216 | return one 217 | } 218 | 219 | func MergeMapS(one map[string]string, extra ...map[string]string) map[string]string { 220 | for _, e := range extra { 221 | for k, v := range e { 222 | one[k] = v 223 | } 224 | } 225 | return one 226 | } 227 | 228 | func read16(b []byte, keys []string, offset int) (int, error) { 229 | start, err := getStartByte(b, keys, offset) 230 | if err != nil { 231 | return -1, nil 232 | } 233 | r := binary.BigEndian.Uint16(b[start+offset:]) 234 | return int(r), nil 235 | } 236 | 237 | func read32(b []byte, keys []string, offset int) (int, error) { 238 | start, err := getStartByte(b, keys, offset) 239 | if err != nil { 240 | return -1, nil 241 | } 242 | r := binary.BigEndian.Uint32(b[start+offset:]) 243 | return int(r), nil 244 | } 245 | 246 | func getStartByte(b []byte, keys []string, offset int) (int, error) { 247 | start := 0 248 | for _, key := range keys { 249 | n := bytes.Index(b[start:], []byte(key)) 250 | if n == -1 { 251 | return -1, ErrByteIndexNotFound 252 | } 253 | start += n + len(key) 254 | } 255 | return start, nil 256 | } 257 | 258 | func getSupCap() (string, error) { 259 | query := []trayRequest{ 260 | {"SUPPORTED_SDK_VERSIONS", supportedSdkVersions}, 261 | {"FACE_TRACKER_VERSION", facetrackerVersion}, 262 | {"segmentation", segmentation}, 263 | {"COMPRESSION", compression}, 264 | {"world_tracker", worldTracker}, 265 | {"gyroscope", gyroscope}, 266 | } 267 | data, err := json.Marshal(query) 268 | if err != nil { 269 | return "", err 270 | } 271 | return string(data), nil 272 | } 273 | 274 | func randNum(l int) string { 275 | var num string 276 | for i := 0; i < l; i++ { 277 | num += toString(random(0, 9)) 278 | } 279 | return num 280 | } 281 | 282 | // checkHeadlessErr will return a proper error if a chrome browser was not found. 283 | func checkHeadlessErr(err error) error { 284 | // Check if err = Chrome not found 285 | if err != nil { 286 | if matched, reErr := regexp.Match("executable file not found", []byte(err.Error())); reErr != nil { 287 | return reErr 288 | } else if matched { 289 | return ErrChromeNotFound 290 | } 291 | return err 292 | } 293 | return nil 294 | } 295 | 296 | func errIsFatal(err error) bool { 297 | switch err { 298 | case ErrBadPassword: 299 | fallthrough 300 | case Err2FARequired: 301 | fallthrough 302 | case ErrLoggedOut: 303 | fallthrough 304 | case ErrLoginRequired: 305 | return true 306 | default: 307 | return false 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /wrapper.go: -------------------------------------------------------------------------------- 1 | package goinsta 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TooManyRequestsTimeout = 60 * time.Second * 8 12 | ) 13 | 14 | type ReqWrapper interface { 15 | GoInstaWrapper(*ReqWrapperArgs) (body []byte, h http.Header, err error) 16 | } 17 | 18 | type ReqWrapperArgs struct { 19 | insta *Instagram 20 | reqOptions *reqOptions 21 | 22 | Body []byte 23 | Headers http.Header 24 | Error error 25 | } 26 | 27 | type Wrapper struct { 28 | o *ReqWrapperArgs 29 | } 30 | 31 | func (w *ReqWrapperArgs) RetryRequest() (body []byte, h http.Header, err error) { 32 | return w.insta.sendRequest(w.reqOptions) 33 | } 34 | 35 | func (w *ReqWrapperArgs) GetWrapperCount() int { 36 | return w.reqOptions.WrapperCount 37 | } 38 | 39 | func (w *ReqWrapperArgs) GetInsta() *Instagram { 40 | return w.insta 41 | } 42 | 43 | func (w *ReqWrapperArgs) GetEndpoint() string { 44 | return w.reqOptions.Endpoint 45 | } 46 | 47 | func (w *ReqWrapperArgs) SetInsta(insta *Instagram) { 48 | w.insta = insta 49 | } 50 | 51 | func (w *ReqWrapperArgs) Ignore429() bool { 52 | return w.reqOptions.Ignore429 53 | } 54 | 55 | func DefaultWrapper() *Wrapper { 56 | return &Wrapper{} 57 | } 58 | 59 | // GoInstaWrapper is a warpper function for goinsta 60 | func (w *Wrapper) GoInstaWrapper(o *ReqWrapperArgs) ([]byte, http.Header, error) { 61 | // If no errors occured, directly return 62 | if o.Error == nil { 63 | return o.Body, o.Headers, o.Error 64 | } 65 | 66 | // If wrapper called more than threshold, return 67 | if o.GetWrapperCount() > 3 { 68 | return o.Body, o.Headers, o.Error 69 | } 70 | 71 | w.o = o 72 | insta := o.GetInsta() 73 | 74 | switch true { 75 | case errors.Is(o.Error, ErrTooManyRequests): 76 | // Some endpoints often return 429, too many requests, and can be safely ignored. 77 | if o.Ignore429() { 78 | return o.Body, o.Headers, nil 79 | } 80 | insta.warnHandler("Too many requests, sleeping for ", TooManyRequestsTimeout) 81 | time.Sleep(TooManyRequestsTimeout) 82 | 83 | case errors.Is(o.Error, Err2FARequired): 84 | // Attempt auto 2FA login with TOTP code generation 85 | err := insta.TwoFactorInfo.Login2FA() 86 | if err != nil && err != Err2FANoCode { 87 | return o.Body, o.Headers, err 88 | } else { 89 | return o.Body, o.Headers, o.Error 90 | } 91 | 92 | case errors.Is(o.Error, ErrLoggedOut): 93 | fallthrough 94 | case errors.Is(o.Error, ErrLoginRequired): 95 | return o.Body, o.Headers, o.Error 96 | 97 | case errors.Is(o.Error, ErrCheckpointRequired): 98 | // Attempt to accecpt cookies using headless browser 99 | err := insta.Checkpoint.Process() 100 | if err != nil { 101 | return o.Body, o.Headers, fmt.Errorf( 102 | "failed to automatically process status code 400 'checkpoint_required' with checkpoint url '%s', please report this on github. Error provided: %w", 103 | insta.Checkpoint.URL, 104 | err, 105 | ) 106 | } 107 | insta.infoHandler( 108 | fmt.Sprintf("Auto solving of checkpoint with url '%s' seems to have gone successful. This is an experimental feature, please let me know if it works! :)\n", 109 | insta.Checkpoint.URL, 110 | )) 111 | 112 | case errors.Is(o.Error, ErrCheckpointPassed): 113 | // continue without doing anything, retry request 114 | 115 | case errors.Is(o.Error, ErrChallengeRequired): 116 | if err := insta.Challenge.Process(); err != nil { 117 | return o.Body, o.Headers, fmt.Errorf("failed to process challenge automatically with: %w", err) 118 | } 119 | default: 120 | // Unhandeled errors should be passed on 121 | return o.Body, o.Headers, o.Error 122 | } 123 | 124 | body, h, err := w.o.RetryRequest() 125 | return body, h, err 126 | } 127 | --------------------------------------------------------------------------------