-
Notifications
You must be signed in to change notification settings - Fork 56
/
timeline.go
365 lines (316 loc) · 9.05 KB
/
timeline.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package goinsta
import (
"bytes"
"encoding/json"
"math/rand"
"sync"
"time"
)
type (
fetchReason string
)
var (
PULLTOREFRESH fetchReason = "pull_to_refresh"
COLDSTART fetchReason = "cold_start_fetch"
WARMSTART fetchReason = "warm_start_fetch"
PAGINATION fetchReason = "pagination"
AUTOREFRESH fetchReason = "auto_refresh" // so far unused
)
// Timeline is the object to represent the main feed on instagram, the first page that shows the latest feeds of my following contacts.
type Timeline struct {
insta *Instagram
err error
lastRequest int64
pullRefresh bool
sessionID string
prevReason fetchReason
fetchExtra bool
endpoint string
Items []*Item
Tray *Tray
MoreAvailable bool
NextID string
NumResults float64
PreloadDistance float64
PullToRefreshWindowMs float64
RequestID string
SessionID string
}
type feedCache struct {
Items []struct {
MediaOrAd *Item `json:"media_or_ad"`
EndOfFeed struct {
Pause bool `json:"pause"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
} `json:"end_of_feed_demarcator"`
} `json:"feed_items"`
MoreAvailable bool `json:"more_available"`
NextID string `json:"next_max_id"`
NumResults float64 `json:"num_results"`
PullToRefreshWindowMs float64 `json:"pull_to_refresh_window_ms"`
RequestID string `json:"request_id"`
SessionID string `json:"session_id"`
ViewStateVersion string `json:"view_state_version"`
AutoLoadMore bool `json:"auto_load_more_enabled"`
IsDirectV2Enabled bool `json:"is_direct_v2_enabled"`
ClientFeedChangelistApplied bool `json:"client_feed_changelist_applied"`
PreloadDistance float64 `json:"preload_distance"`
Status string `json:"status"`
FeedPillText string `json:"feed_pill_text"`
StartupPrefetchConfigs struct {
Explore struct {
ContainerModule string `json:"containermodule"`
ShouldPrefetch bool `json:"should_prefetch"`
ShouldPrefetchThumbnails bool `json:"should_prefetch_thumbnails"`
} `json:"explore"`
} `json:"startup_prefetch_configs"`
UseAggressiveFirstTailLoad bool `json:"use_aggressive_first_tail_load"`
HideLikeAndViewCounts float64 `json:"hide_like_and_view_counts"`
}
func newTimeline(insta *Instagram) *Timeline {
time := &Timeline{
insta: insta,
endpoint: urlTimeline,
}
return time
}
// Next allows pagination after calling:
// User.Feed
// returns false when list reach the end.
// if Timeline.Error() is ErrNoMore no problem have been occurred.
// starts first request will be a cold start
func (tl *Timeline) Next(p ...interface{}) bool {
if tl.err != nil {
return false
}
insta := tl.insta
endpoint := tl.endpoint
// make sure at least 4 sec after last request, at most 6 sec
var th int64 = 4
var thR float64 = 2
// if fetching extra, no big timeout is needed
if tl.fetchExtra {
th = 2
thR = 1
}
if delta := time.Now().Unix() - tl.lastRequest; delta < th {
s := time.Duration(rand.Float64()*thR + float64(th-delta))
time.Sleep(s * time.Second)
}
t := time.Now().Unix()
var reason fetchReason
isPullToRefresh := "0"
query := map[string]string{
"feed_view_info": "[]",
"timezone_offset": timeOffset,
"device_id": insta.uuid,
"request_id": generateUUID(),
"_uuid": insta.uuid,
"bloks_versioning_id": bloksVerID,
}
var tWarm int64 = 10
if tl.pullRefresh || (!tl.MoreAvailable && t-tl.lastRequest < tWarm*60) {
reason = PULLTOREFRESH
isPullToRefresh = "1"
} else if tl.lastRequest == 0 || (tl.fetchExtra && tl.prevReason == "warm_start_fetch") {
reason = COLDSTART
} else if t-tl.lastRequest > tWarm*60 { // 10 min
reason = WARMSTART
} else if tl.fetchExtra || tl.MoreAvailable && tl.NextID != "" {
reason = PAGINATION
query["max_id"] = tl.NextID
}
wg := &sync.WaitGroup{}
defer wg.Wait()
errChan := make(chan error)
if reason != PAGINATION {
tl.sessionID = generateUUID()
wg.Add(1)
go func() {
defer wg.Done()
err := tl.FetchTray(reason)
if err != nil {
errChan <- err
}
}()
}
query["reason"] = string(reason)
query["is_pull_to_refresh"] = isPullToRefresh
query["session_id"] = tl.sessionID
tl.prevReason = reason
body, _, err := insta.sendRequest(
&reqOptions{
Endpoint: endpoint,
IsPost: true,
Gzip: true,
Query: query,
ExtraHeaders: map[string]string{
"X-Ads-Opt-Out": "0",
"X-Google-AD-ID": insta.adid,
"X-Fb": "1",
},
},
)
if err != nil {
tl.err = err
return false
}
tl.lastRequest = t
// Decode json
tmp := feedCache{}
d := json.NewDecoder(bytes.NewReader(body))
d.UseNumber()
err = d.Decode(&tmp)
// Add posts to Timeline object
if err != nil {
tl.err = err
return false
}
// copy constants over
tl.NextID = tmp.NextID
tl.MoreAvailable = tmp.MoreAvailable
if tl.fetchExtra {
tl.NumResults += tmp.NumResults
} else {
tl.NumResults = tmp.NumResults
}
tl.PreloadDistance = tmp.PreloadDistance
tl.PullToRefreshWindowMs = tmp.PullToRefreshWindowMs
tl.fetchExtra = false
// copy post items over
for _, i := range tmp.Items {
// will be nil if end of feed, EndOfFeed will then be set
if i.MediaOrAd != nil {
setToItem(i.MediaOrAd, tl)
tl.Items = append(tl.Items, i.MediaOrAd)
}
}
// Set index value
for i, v := range tl.Items {
v.Index = i
}
// fetch more posts if not enough posts were returned, mimick apk behvaior
if reason != PULLTOREFRESH && tmp.NumResults < tmp.PreloadDistance && tmp.MoreAvailable {
tl.fetchExtra = true
tl.Next()
}
// Check if stories returned an error
select {
case err := <-errChan:
if err != nil {
tl.err = err
return false
}
default:
}
if !tl.MoreAvailable {
tl.err = ErrNoMore
return false
}
return true
}
// SetPullRefresh will set a flag to refresh the timeline on subsequent .Next() call
func (tl *Timeline) SetPullRefresh() {
tl.pullRefresh = true
}
// UnsetPullRefresh will unset the pull to refresh flag, if you previously manually
// set it, and want to unset it.
func (tl *Timeline) UnsetPullRefresh() {
tl.pullRefresh = false
}
// ClearPosts will unreference the current list of post items. Used when calling
// .Refresh()
func (tl *Timeline) ClearPosts() {
tl.Items = []*Item{}
tl.Tray = &Tray{}
}
// FetchTray fetches the timeline tray with story media.
// This function should rarely be called manually. If you want to refresh
// the timeline call Timeline.Refresh()
func (tl *Timeline) FetchTray(r fetchReason) error {
insta := tl.insta
var reason string
switch r {
case PULLTOREFRESH:
reason = string(PULLTOREFRESH)
case COLDSTART:
reason = "cold_start"
case WARMSTART:
reason = "warm_start_with_feed"
}
body, _, err := insta.sendRequest(
&reqOptions{
Endpoint: urlStories,
IsPost: true,
Query: map[string]string{
"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"}]`,
"reason": reason,
"timezone_offset": timeOffset,
"tray_session_id": generateUUID(),
"request_id": generateUUID(),
"_uuid": tl.insta.uuid,
},
},
)
if err != nil {
return err
}
tray := &Tray{}
err = json.Unmarshal(body, tray)
if err != nil {
return err
}
tray.set(tl.insta)
tl.Tray = tray
return nil
}
// Refresh will clear the current list of posts, perform a pull to refresh action,
// and refresh the current timeline.
func (tl *Timeline) Refresh() error {
tl.ClearPosts()
tl.SetPullRefresh()
if !tl.Next() {
return tl.err
}
return nil
}
// NewFeedPostsExist will return true if new feed posts are available.
func (tl *Timeline) NewFeedPostsExist() (bool, error) {
insta := tl.insta
body, err := insta.sendSimpleRequest(urlFeedNewPostsExist)
if err != nil {
return false, err
}
var resp struct {
NewPosts bool `json:"new_feed_posts_exist"`
Status string `json:"status"`
}
err = json.Unmarshal(body, &resp)
if err != nil {
return false, err
}
return resp.NewPosts, nil
}
// Stories is a helper function to get the stories
func (tl *Timeline) Stories() []*Reel {
return tl.Tray.Stories
}
// helper function to get the Broadcasts
func (tl *Timeline) Broadcasts() []*Broadcast {
return tl.Tray.Broadcasts
}
func (tl *Timeline) GetNextID() string {
return tl.NextID
}
// Delete is only a placeholder, it does nothing
func (tl *Timeline) Delete() error {
return nil
}
func (tl *Timeline) getInsta() *Instagram {
return tl.insta
}
// Error will the error of the Timeline instance if one occured
func (tl *Timeline) Error() error {
return tl.err
}