2020-01-12 12:50:21 +00:00
const constants = require ( "./constants" )
const { request } = require ( "./utils/request" )
2020-02-02 11:43:56 +00:00
const switcher = require ( "./utils/torswitcher" )
2020-01-12 12:50:21 +00:00
const { extractSharedData } = require ( "./utils/body" )
2020-02-02 14:53:37 +00:00
const { TtlCache , RequestCache , UserRequestCache } = require ( "./cache" )
2020-01-30 12:51:59 +00:00
const RequestHistory = require ( "./structures/RequestHistory" )
2020-02-01 04:44:40 +00:00
const db = require ( "./db" )
2020-02-18 04:06:11 +00:00
require ( "./testimports" ) ( constants , request , extractSharedData , UserRequestCache , RequestHistory , db )
2020-01-12 12:50:21 +00:00
2020-01-28 03:14:21 +00:00
const requestCache = new RequestCache ( constants . caching . resource _cache _time )
2020-04-04 14:57:31 +00:00
/** @type {import("./cache").UserRequestCache<import("./structures/User")|import("./structures/ReelUser")>} */
2020-02-02 14:53:37 +00:00
const userRequestCache = new UserRequestCache ( constants . caching . resource _cache _time )
2020-01-26 14:56:59 +00:00
/** @type {import("./cache").TtlCache<import("./structures/TimelineEntry")>} */
2020-01-28 03:14:21 +00:00
const timelineEntryCache = new TtlCache ( constants . caching . resource _cache _time )
2020-06-24 14:58:01 +00:00
const history = new RequestHistory ( [ "user" , "timeline" , "igtv" , "post" , "reel" ] )
2020-01-12 12:50:21 +00:00
2020-04-07 06:30:00 +00:00
const AssistantSwitcher = require ( "./structures/AssistantSwitcher" )
const assistantSwitcher = new AssistantSwitcher ( )
2020-02-18 00:39:20 +00:00
/ * *
* @ param { string } username
2020-04-12 14:52:04 +00:00
* @ param { symbol } [ context ]
2020-02-18 00:39:20 +00:00
* /
2020-04-12 14:52:04 +00:00
async function fetchUser ( username , context ) {
if ( constants . external . reserved _paths . includes ( username ) ) {
throw constants . symbols . ENDPOINT _OVERRIDDEN
}
2020-02-02 14:53:37 +00:00
let mode = constants . allow _user _from _reel
if ( mode === "preferForRSS" ) {
2020-04-12 14:52:04 +00:00
if ( context === constants . symbols . fetch _context . RSS ) mode = "prefer"
2020-02-18 04:06:11 +00:00
else mode = "onlyPreferSaved"
2020-02-02 14:53:37 +00:00
}
2020-04-12 14:52:04 +00:00
if ( context === constants . symbols . fetch _context . ASSISTANT ) {
const saved = db . prepare ( "SELECT username, user_id, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url FROM Users WHERE username = ?" ) . get ( username )
if ( saved && saved . updated _version >= 2 ) {
return fetchUserFromSaved ( saved )
} else {
return fetchUserFromHTML ( username )
}
}
2020-02-02 14:53:37 +00:00
if ( mode === "never" ) {
2020-02-02 13:24:14 +00:00
return fetchUserFromHTML ( username )
2020-02-18 04:06:11 +00:00
}
if ( mode === "prefer" ) {
const saved = db . prepare ( "SELECT username, user_id, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url FROM Users WHERE username = ?" ) . get ( username )
if ( saved && saved . updated _version >= 2 ) {
return fetchUserFromSaved ( saved )
} else if ( saved && saved . updated _version === 1 ) {
return fetchUserFromCombined ( saved . user _id , saved . username )
} else {
return fetchUserFromHTML ( username )
}
}
if ( mode === "onlyPreferSaved" ) {
const saved = db . prepare ( "SELECT username, user_id, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url FROM Users WHERE username = ?" ) . get ( username )
if ( saved && saved . updated _version >= 2 ) {
return fetchUserFromSaved ( saved )
} else {
mode = "fallback"
}
}
if ( mode === "fallback" ) {
2020-02-02 13:24:14 +00:00
return fetchUserFromHTML ( username ) . catch ( error => {
if ( error === constants . symbols . INSTAGRAM _DEMANDS _LOGIN || error === constants . symbols . RATE _LIMITED ) {
2020-02-18 04:06:11 +00:00
const saved = db . prepare ( "SELECT username, user_id, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url FROM Users WHERE username = ?" ) . get ( username )
if ( saved && saved . updated _version === 1 ) {
return fetchUserFromCombined ( saved . user _id , username )
} else if ( saved && saved . updated _version >= 2 ) {
return fetchUserFromSaved ( saved )
2020-04-07 06:30:00 +00:00
} else if ( assistantSwitcher . enabled ( ) ) {
return assistantSwitcher . requestUser ( username ) . catch ( error => {
if ( error === constants . symbols . NO _ASSISTANTS _AVAILABLE ) throw constants . symbols . RATE _LIMITED
else throw error
} )
2020-02-02 13:45:03 +00:00
}
2020-02-02 13:24:14 +00:00
}
throw error
} )
}
2020-02-18 04:06:11 +00:00
throw new Error ( ` Selected fetch mode ${ mode } was unmatched. ` )
2020-02-02 13:24:14 +00:00
}
2020-02-18 00:39:20 +00:00
/ * *
* @ param { string } username
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { user : import ( "./structures/User" ) , quotaUsed : number } > }
2020-02-18 00:39:20 +00:00
* /
2020-02-02 13:24:14 +00:00
function fetchUserFromHTML ( username ) {
2020-04-19 13:57:21 +00:00
if ( constants . caching . self _blocked _status . enabled ) {
if ( history . store . has ( "user" ) ) {
const entry = history . store . get ( "user" )
if ( ! entry . lastRequestSuccessful && Date . now ( ) < entry . lastRequestAt + constants . caching . self _blocked _status . time ) {
return Promise . reject ( constants . symbols . RATE _LIMITED )
2020-04-16 13:40:20 +00:00
}
}
2020-04-19 13:57:21 +00:00
}
return userRequestCache . getOrFetch ( "user/" + username , false , true , ( ) => {
2020-07-11 08:56:19 +00:00
return switcher . request ( "user_html" , ` https://www.instagram.com/ ${ username } /feed/ ` , async res => {
2020-02-05 12:32:51 +00:00
if ( res . status === 301 ) throw constants . symbols . ENDPOINT _OVERRIDDEN
2020-02-02 13:24:14 +00:00
if ( res . status === 302 ) throw constants . symbols . INSTAGRAM _DEMANDS _LOGIN
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
return res
2020-03-15 06:50:29 +00:00
} ) . then ( async g => {
const res = await g . response ( )
2020-02-02 13:24:14 +00:00
if ( res . status === 404 ) {
2020-01-30 12:51:59 +00:00
throw constants . symbols . NOT _FOUND
2020-02-02 13:24:14 +00:00
} else {
2020-03-15 06:50:29 +00:00
const text = await g . text ( )
// require down here or have to deal with require loop. require cache will take care of it anyway.
// User -> Timeline -> TimelineEntry -> collectors -/> User
const User = require ( "./structures/User" )
2020-04-13 15:46:23 +00:00
const result = extractSharedData ( text )
if ( result . status === constants . symbols . extractor _results . SUCCESS ) {
const sharedData = result . value
const user = new User ( sharedData . entry _data . ProfilePage [ 0 ] . graphql . user )
history . report ( "user" , true )
if ( constants . caching . db _user _id ) {
const existing = db . prepare ( "SELECT created, updated_version FROM Users WHERE username = ?" ) . get ( user . data . username )
db . prepare (
"REPLACE INTO Users (username, user_id, created, updated, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url) VALUES "
2020-04-16 13:40:20 +00:00
+ "(@username, @user_id, @created, @updated, @updated_version, @biography, @post_count, @following_count, @followed_by_count, @external_url, @full_name, @is_private, @is_verified, @profile_pic_url)"
2020-04-13 15:46:23 +00:00
) . run ( {
username : user . data . username ,
user _id : user . data . id ,
created : existing && existing . updated _version === constants . database _version ? existing . created : Date . now ( ) ,
updated : Date . now ( ) ,
updated _version : constants . database _version ,
biography : user . data . biography || null ,
post _count : user . posts || 0 ,
following _count : user . following || 0 ,
followed _by _count : user . followedBy || 0 ,
external _url : user . data . external _url || null ,
full _name : user . data . full _name || null ,
is _private : + user . data . is _private ,
is _verified : + user . data . is _verified ,
profile _pic _url : user . data . profile _pic _url
} )
}
return user
} else if ( result . status === constants . symbols . extractor _results . AGE _RESTRICTED ) {
// I don't like this code.
history . report ( "user" , true )
throw constants . symbols . extractor _results . AGE _RESTRICTED
} else {
throw result . status
2020-03-15 06:50:29 +00:00
}
2020-02-02 13:24:14 +00:00
}
} ) . catch ( error => {
if ( error === constants . symbols . INSTAGRAM _DEMANDS _LOGIN || error === constants . symbols . RATE _LIMITED ) {
history . report ( "user" , false )
}
throw error
2020-01-12 12:50:21 +00:00
} )
2020-07-22 12:58:21 +00:00
} ) . then ( user => ( { user , quotaUsed : 0 } ) )
2020-01-12 12:50:21 +00:00
}
2020-04-04 14:57:31 +00:00
/ * *
* @ param { string } userID
* /
function updateProfilePictureFromReel ( userID ) {
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . reel _query _hash )
p . set ( "variables" , JSON . stringify ( {
user _id : userID ,
include _reel : true
} ) )
return switcher . request ( "reel_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
return res
} ) . then ( res => res . json ( ) ) . then ( root => {
const result = root . data . user
if ( ! result ) throw constants . symbols . NOT _FOUND
const profilePicURL = result . reel . user . profile _pic _url
if ( ! profilePicURL ) throw constants . symbols . NOT _FOUND
db . prepare ( "UPDATE Users SET profile_pic_url = ? WHERE user_id = ?" ) . run ( profilePicURL , userID )
2020-04-20 07:12:01 +00:00
for ( const entry of userRequestCache . cache . values ( ) ) {
2020-04-04 14:57:31 +00:00
// yes, data.data is correct.
2020-04-20 07:12:01 +00:00
if ( entry . data && entry . data . data && entry . data . data . id === userID ) {
entry . data . data . profile _pic _url = profilePicURL
entry . data . computeProxyProfilePic ( )
2020-04-04 14:57:31 +00:00
break // stop checking entries from the cache since we won't find any more
}
}
return profilePicURL
} ) . catch ( error => {
throw error
} )
}
2020-02-18 00:39:20 +00:00
/ * *
* @ param { string } userID
* @ param { string } username
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { user : import ( "./structures/ReelUser" ) | import ( "./structures/User" ) , quotaUsed : number } > }
2020-02-18 00:39:20 +00:00
* /
2020-02-02 13:24:14 +00:00
function fetchUserFromCombined ( userID , username ) {
// Fetch basic user information
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . reel _query _hash )
p . set ( "variables" , JSON . stringify ( {
user _id : userID ,
include _reel : true
} ) )
2020-02-02 14:53:37 +00:00
return userRequestCache . getOrFetch ( "user/" + username , true , false , ( ) => {
2020-02-02 13:24:14 +00:00
return switcher . request ( "reel_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
return res
} ) . then ( res => res . json ( ) ) . then ( root => {
const result = root . data . user
2020-07-07 10:08:19 +00:00
if ( ! result ) {
// user ID doesn't exist.
db . prepare ( "DELETE FROM Users WHERE user_id = ?" ) . run ( userID ) // deleting the entry makes sense to me; the username might be claimed by somebody else later
throw constants . symbols . NOT _FOUND // this should cascade down and show the user not found page
}
2020-02-02 13:24:14 +00:00
// require down here or have to deal with require loop. require cache will take care of it anyway.
// ReelUser -> Timeline -> TimelineEntry -> collectors -/> User
const ReelUser = require ( "./structures/ReelUser" )
const user = new ReelUser ( result . reel . user )
2020-02-18 04:06:11 +00:00
history . report ( "reel" , true )
2020-02-02 13:24:14 +00:00
return user
} )
} ) . then ( async user => {
// Add first timeline page
2020-07-22 12:58:21 +00:00
let quotaUsed = 0
2020-02-02 13:24:14 +00:00
if ( ! user . timeline . pages [ 0 ] ) {
2020-07-22 12:58:21 +00:00
const fetched = await fetchTimelinePage ( userID , "" )
if ( ! fetched . fromCache ) quotaUsed ++
user . timeline . addPage ( fetched . result )
2020-02-02 13:24:14 +00:00
}
2020-07-22 12:58:21 +00:00
return { user , quotaUsed }
2020-02-02 13:44:52 +00:00
} ) . catch ( error => {
if ( error === constants . symbols . RATE _LIMITED ) {
history . report ( "reel" , false )
}
throw error
2020-02-02 13:24:14 +00:00
} )
}
2020-02-18 04:06:11 +00:00
function fetchUserFromSaved ( saved ) {
2020-07-22 12:58:21 +00:00
let quotaUsed = 0
2020-02-18 04:06:11 +00:00
return userRequestCache . getOrFetch ( "user/" + saved . username , false , true , async ( ) => {
// require down here or have to deal with require loop. require cache will take care of it anyway.
// ReelUser -> Timeline -> TimelineEntry -> collectors -/> ReelUser
const ReelUser = require ( "./structures/ReelUser" )
const user = new ReelUser ( {
username : saved . username ,
id : saved . user _id ,
biography : saved . biography ,
edge _follow : { count : saved . following _count } ,
edge _followed _by : { count : saved . followed _by _count } ,
external _url : saved . external _url ,
full _name : saved . full _name ,
is _private : ! ! saved . is _private ,
is _verified : ! ! saved . is _verified ,
profile _pic _url : saved . profile _pic _url
} )
// Add first timeline page
if ( ! user . timeline . pages [ 0 ] ) {
2020-07-22 12:58:21 +00:00
const { result : page , fromCache } = await fetchTimelinePage ( user . data . id , "" )
if ( ! fromCache ) quotaUsed ++
2020-02-18 04:06:11 +00:00
user . timeline . addPage ( page )
}
return user
2020-07-22 12:58:21 +00:00
} ) . then ( user => {
return { user , quotaUsed }
2020-02-18 04:06:11 +00:00
} )
}
2020-01-12 12:50:21 +00:00
/ * *
* @ param { string } userID
* @ param { string } after
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { result : import ( "./types" ) . PagedEdges < import ( "./types" ) . TimelineEntryN2 > , fromCache : boolean } > }
2020-01-12 12:50:21 +00:00
* /
function fetchTimelinePage ( userID , after ) {
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . timeline _query _hash )
p . set ( "variables" , JSON . stringify ( {
id : userID ,
first : constants . external . timeline _fetch _first ,
after : after
} ) )
2020-02-02 13:24:14 +00:00
return requestCache . getOrFetchPromise ( ` page/ ${ userID } / ${ after } ` , ( ) => {
return switcher . request ( "timeline_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
2020-02-02 11:43:56 +00:00
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
2020-03-15 06:50:29 +00:00
} ) . then ( g => g . json ( ) ) . then ( root => {
2020-07-07 10:08:19 +00:00
if ( root . data . user === null ) {
// user ID doesn't exist.
db . prepare ( "DELETE FROM Users WHERE user_id = ?" ) . run ( userID ) // deleting the entry makes sense to me; the username might be claimed by somebody else later
requestCache
throw constants . symbols . NOT _FOUND // this should cascade down and show the user not found page
}
2020-02-02 11:43:56 +00:00
/** @type {import("./types").PagedEdges<import("./types").TimelineEntryN2>} */
const timeline = root . data . user . edge _owner _to _timeline _media
history . report ( "timeline" , true )
return timeline
} ) . catch ( error => {
if ( error === constants . symbols . RATE _LIMITED ) {
2020-01-30 12:51:59 +00:00
history . report ( "timeline" , false )
}
2020-02-02 11:43:56 +00:00
throw error
2020-01-12 12:50:21 +00:00
} )
} )
}
2020-06-11 16:09:28 +00:00
/ * *
* @ param { string } userID
* @ param { string } after
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { result : import ( "./types" ) . PagedEdges < import ( "./types" ) . TimelineEntryN2 > , fromCache : boolean } > }
2020-06-11 16:09:28 +00:00
* /
function fetchIGTVPage ( userID , after ) {
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . igtv _query _hash )
p . set ( "variables" , JSON . stringify ( {
id : userID ,
first : constants . external . igtv _fetch _first ,
after : after
} ) )
return requestCache . getOrFetchPromise ( ` igtv/ ${ userID } / ${ after } ` , ( ) => {
// assuming this uses the same bucket as timeline, which may not be the case
return switcher . request ( "timeline_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
} ) . then ( g => g . json ( ) ) . then ( root => {
/** @type {import("./types").PagedEdges<import("./types").TimelineEntryN2>} */
2020-06-24 14:58:01 +00:00
const timeline = root . data . user . edge _felix _video _timeline
history . report ( "igtv" , true )
2020-06-11 16:09:28 +00:00
return timeline
} ) . catch ( error => {
if ( error === constants . symbols . RATE _LIMITED ) {
2020-06-24 14:58:01 +00:00
history . report ( "igtv" , false )
2020-06-11 16:09:28 +00:00
}
throw error
} )
} )
}
/ * *
* @ param { string } userID
* @ param { string } username
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { result : boolean , fromCache : boolean } > }
2020-06-11 16:09:28 +00:00
* /
function verifyUserPair ( userID , username ) {
// Fetch basic user information
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . reel _query _hash )
p . set ( "variables" , JSON . stringify ( {
user _id : userID ,
include _reel : true
} ) )
return requestCache . getOrFetchPromise ( "userID/" + userID , ( ) => {
return switcher . request ( "reel_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
return res
} ) . then ( res => res . json ( ) ) . then ( root => {
let user = root . data . user
if ( ! user ) throw constants . symbols . NOT _FOUND
user = user . reel . user
history . report ( "reel" , true )
return user . id === userID && user . username === username
} ) . catch ( error => {
throw error
} )
} )
}
2020-01-18 15:38:14 +00:00
/ * *
* @ param { string } shortcode
2020-01-26 14:56:59 +00:00
* @ returns { import ( "./structures/TimelineEntry" ) }
2020-01-18 15:38:14 +00:00
* /
2020-01-26 14:56:59 +00:00
function getOrCreateShortcode ( shortcode ) {
if ( timelineEntryCache . has ( shortcode ) ) {
return timelineEntryCache . get ( shortcode )
} else {
// require down here or have to deal with require loop. require cache will take care of it anyway.
2020-02-02 13:24:14 +00:00
// TimelineEntry -> collectors -/> TimelineEntry
2020-01-26 14:56:59 +00:00
const TimelineEntry = require ( "./structures/TimelineEntry" )
const result = new TimelineEntry ( )
timelineEntryCache . set ( shortcode , result )
return result
}
}
2020-01-18 15:38:14 +00:00
2020-01-26 14:56:59 +00:00
async function getOrFetchShortcode ( shortcode ) {
if ( timelineEntryCache . has ( shortcode ) ) {
2020-07-29 09:51:41 +00:00
return { post : timelineEntryCache . get ( shortcode ) , fromCache : true }
2020-01-26 14:56:59 +00:00
} else {
2020-07-29 09:51:41 +00:00
const { result , fromCache } = await fetchShortcodeData ( shortcode )
2020-01-26 14:56:59 +00:00
const entry = getOrCreateShortcode ( shortcode )
2020-07-29 09:51:41 +00:00
entry . applyN3 ( result )
return { post : entry , fromCache }
2020-01-26 14:56:59 +00:00
}
}
/ * *
* @ param { string } shortcode
2020-07-22 12:58:21 +00:00
* @ returns { Promise < { result : import ( "./types" ) . TimelineEntryN3 , fromCache : boolean } > }
2020-01-26 14:56:59 +00:00
* /
function fetchShortcodeData ( shortcode ) {
2020-01-18 15:38:14 +00:00
// example actual query from web:
// query_hash=2b0673e0dc4580674a88d426fe00ea90&variables={"shortcode":"xxxxxxxxxxx","child_comment_count":3,"fetch_comment_count":40,"parent_comment_count":24,"has_threaded_comments":true}
// we will not include params about comments, which means we will not receive comments, but everything else should still work fine
const p = new URLSearchParams ( )
p . set ( "query_hash" , constants . external . shortcode _query _hash )
p . set ( "variables" , JSON . stringify ( { shortcode } ) )
return requestCache . getOrFetchPromise ( "shortcode/" + shortcode , ( ) => {
2020-02-02 13:24:14 +00:00
return switcher . request ( "post_graphql" , ` https://www.instagram.com/graphql/query/? ${ p . toString ( ) } ` , async res => {
2020-02-02 11:43:56 +00:00
if ( res . status === 429 ) throw constants . symbols . RATE _LIMITED
} ) . then ( res => res . json ( ) ) . then ( root => {
/** @type {import("./types").TimelineEntryN3} */
const data = root . data . shortcode _media
if ( data == null ) {
// the thing doesn't exist
throw constants . symbols . NOT _FOUND
2020-01-27 06:03:28 +00:00
} else {
2020-02-02 11:43:56 +00:00
history . report ( "post" , true )
if ( constants . caching . db _post _n3 ) {
db . prepare ( "REPLACE INTO Posts (shortcode, id, id_as_numeric, username, json) VALUES (@shortcode, @id, @id_as_numeric, @username, @json)" )
. run ( { shortcode : data . shortcode , id : data . id , id _as _numeric : data . id , username : data . owner . username , json : JSON . stringify ( data ) } )
2020-01-30 12:51:59 +00:00
}
2020-02-02 13:24:14 +00:00
// if we have the owner but only a reelUser, update it. this code is gross.
2020-02-02 14:53:37 +00:00
if ( userRequestCache . hasNotPromise ( "user/" + data . owner . username ) ) {
const user = userRequestCache . getWithoutClean ( "user/" + data . owner . username )
2020-02-02 13:24:14 +00:00
if ( user . fromReel ) {
user . data . full _name = data . owner . full _name
user . data . is _verified = data . owner . is _verified
}
}
2020-02-02 11:43:56 +00:00
return data
}
} ) . catch ( error => {
if ( error === constants . symbols . RATE _LIMITED ) {
history . report ( "post" , false )
2020-01-27 06:03:28 +00:00
}
2020-02-02 11:43:56 +00:00
throw error
2020-01-18 15:38:14 +00:00
} )
} )
}
2020-01-12 12:50:21 +00:00
module . exports . fetchUser = fetchUser
module . exports . fetchTimelinePage = fetchTimelinePage
2020-06-24 14:58:01 +00:00
module . exports . fetchIGTVPage = fetchIGTVPage
2020-01-26 14:56:59 +00:00
module . exports . getOrCreateShortcode = getOrCreateShortcode
module . exports . fetchShortcodeData = fetchShortcodeData
2020-07-07 10:08:19 +00:00
module . exports . requestCache = requestCache
2020-02-02 14:53:37 +00:00
module . exports . userRequestCache = userRequestCache
2020-01-26 14:56:59 +00:00
module . exports . timelineEntryCache = timelineEntryCache
module . exports . getOrFetchShortcode = getOrFetchShortcode
2020-04-04 14:57:31 +00:00
module . exports . updateProfilePictureFromReel = updateProfilePictureFromReel
2020-01-30 12:51:59 +00:00
module . exports . history = history
2020-04-07 06:30:00 +00:00
module . exports . fetchUserFromSaved = fetchUserFromSaved
module . exports . assistantSwitcher = assistantSwitcher
2020-06-11 16:09:28 +00:00
module . exports . verifyUserPair = verifyUserPair