UNPKG

instagram-nodejs-without-api

Version:

Auth and get followers on instagram with nodejs

622 lines (557 loc) 19.2 kB
/** * @author Alex Yatsenko * @link https://github.com/yatsenkolesh/instagram-nodejs */ "use-strict"; const fetch = require('node-fetch'); const formData = require('form-data'); module.exports = class Instagram { /** * Constructor */ constructor(csrfToken, sessionId) { this.csrfToken = csrfToken this.sessionId = sessionId this.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' this.userIdFollowers = {}; this.timeoutForCounter = 300 this.timeoutForCounterValue = 30000 this.paginationDelay = 30000 this.receivePromises = {} this.searchTypes = ['location', 'hashtag'] this.essentialValues = { sessionid : undefined, ds_user_id : undefined, csrftoken : undefined, shbid : undefined, rur : undefined, mid : undefined, shbts : undefined, mcd : undefined, ig_cb : 1, //urlgen : undefined //this needs to be filled in according to my RE }; this.baseHeader = { 'accept-langauge': 'en-US;q=0.9,en;q=0.8,es;q=0.7', 'origin': 'https://www.instagram.com', 'referer': 'https://www.instagram.com/', 'upgrade-insecure-requests': '1', 'user-agent': this.userAgent, } } generateCookie(simple){ if (simple) return 'ig_cb=1' var cookie = '' var keys = Object.keys(this.essentialValues) for (var i = 0; i < keys.length; i++){ var key = keys[i]; if (this.essentialValues[key] !== undefined) { cookie += key + '=' + this.essentialValues[key] + (i < keys.length - 1 ? '; ' : '') } } return cookie; } combineWithBaseHeader(data){ return Object.assign(this.baseHeader, data) } updateEssentialValues(src, isHTML){ //assumes that essential values will be extracted from a cookie unless specified by the isHTML bool if (!isHTML){ var keys = Object.keys(this.essentialValues) for (var i = 0; i < keys.length; i++){ var key = keys[i]; if (!this.essentialValues[key]) for (let cookie in src) if (src[cookie].includes(key) && !src[cookie].includes(key + '=""')){ var cookieValue = src[cookie].split(';')[0].replace(key + '=', '') this.essentialValues[key] = cookieValue break; } } } else { var subStr = src; var startStr = '<script type="text/javascript">window._sharedData = '; var start = subStr.indexOf(startStr) + startStr.length; subStr = subStr.substr(start, subStr.length); subStr = subStr.substr(0, subStr.indexOf('</script>') - 1); var json = JSON.parse(subStr); this.essentialValues.csrftoken = json.config.csrf_token; this.rollout_hash = json.rollout_hash; } } /** * User data by username * @param {String} username * @return {Object} Promise */ getUserDataByUsername(username) { var fetch_data = { 'method': 'get', 'headers': this.combineWithBaseHeader( { 'accept': 'text/html,application/xhtml+xml,application/xml;q0.9,image/webp,image/apng,*.*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'cookie': this.generateCookie() } ) } return fetch('https://www.instagram.com/' + username, fetch_data).then(res => res.text().then(function (data) { console.log(data) const regex = /window\._sharedData = (.*);<\/script>/; const match = regex.exec(data); if (typeof match[1] === 'undefined') { return ''; } return JSON.parse(match[1]).entry_data.ProfilePage[0]; })) } /** Is private check * @param {String} usernmae */ isPrivate(username) { return this.getUserDataByUsername(username).then((data) => data.user.is_private ) } /** * User followers list * Bench - 1k followers/1 min * @param {Int} userId * @param {String} endCursor cursor used to fetch next page * @param {Int} count count of results to return (API may return less) * @param {Int} followersCounter counter of followers * @param {Boolean} selfSelf if call by self * @return {Object} array followers list */ getUserFollowers(userId, endCursor, count, followersCounter, selfSelf) { const self = this if (!selfSelf) self.userIdFollowers[userId] = [] if (typeof self.receivePromises[userId] !== 'undefined' && !selfSelf) return 0 count = count || 20; const query = { id: userId, include_reel: true, fetch_mutual: true, first: count }; if (endCursor) { query.after = endCursor; } const variables = encodeURIComponent(JSON.stringify(query)); self.receivePromises[userId] = 1 return fetch('https://www.instagram.com/graphql/query/?query_hash=56066f031e6239f35a904ac20c9f37d9&variables=' + variables, { 'method': 'get', 'headers': this.combineWithBaseHeader( { 'accept': 'text/html,application/xhtml+xml,application/xml;q0.9,image/webp,image/apng,*.*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'cookie': this.generateCookie() } ) }).then(res => { return res.text().then((response) => { //prepare convert to json let json = response; try { json = JSON.parse(response) } catch (e) { console.log('Session error') console.log(response) return []; } if (json.status == 'ok') { self.userIdFollowers[userId] = self.userIdFollowers[userId].concat(json.data.user.edge_followed_by.edges) if (json.data.user.edge_followed_by.page_info.has_next_page) { let end_cursor = json.data.user.edge_followed_by.page_info.end_cursor return new Promise((resolve) => { console.log('fetching next page in ' + this.paginationDelay / 1000 + ' seconds'); setTimeout(() => { resolve(self.getUserFollowers(userId, end_cursor, count, 1, 1)); }, this.paginationDelay); }); } else { self.receivePromises[userId] = undefined return self.userIdFollowers[userId] } } else { return new Promise((resolve) => { console.log(json); console.log('request failed, retrying in ' + this.paginationDelay / 1000 + ' seconds'); setTimeout(() => { resolve(self.getUserFollowers(userId, endCursor, count, followersCounter, selfSelf)); }, this.paginationDelay); }); } }).catch((e) => { console.log('Instagram returned:' + e) }) }) } /** * Get csrf token * @return {Object} Promise */ getCsrfToken() { return fetch('https://www.instagram.com', { 'method': 'get', 'headers': this.combineWithBaseHeader( { 'accept': 'text/html,application/xhtml+xml,application/xml;q0.9,image/webp,image/apng,*.*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'cookie': this.generateCookie(true) } ) }).then( t => { this.updateEssentialValues(t.headers._headers['set-cookie']) return t.text() }).then( html => { this.updateEssentialValues(html, true) return this.essentialValues.csrftoken }).catch(() => console.log('Failed to get instagram csrf token') ) } /** * Session id by usrname and password * @param {String} username * @param {String} password * @return {Object} Promise */ auth(username, password) { var formdata = 'username=' + username + '&password=' + password + '&queryParams=%7B%7D' var options = { method : 'POST', body : formdata, headers : this.combineWithBaseHeader( { 'accept' : '*/*', 'accept-encoding' : 'gzip, deflate, br', 'content-length' : formdata.length, 'content-type' : 'application/x-www-form-urlencoded', 'cookie' : 'ig_cb=' + this.essentialValues.ig_cb, 'x-csrftoken' : this.csrfToken, 'x-instagram-ajax' : this.rollout_hash, 'x-requested-with' : 'XMLHttpRequest', } ) } return fetch('https://www.instagram.com/accounts/login/ajax/', options).then( t => { this.updateEssentialValues(t.headers._headers['set-cookie']) return this.essentialValues.sessionid; }).catch(() => console.log('Instagram authentication failed (challenge required erro)') ) } /** * Registration for instagram, returning true or false * true if account was successfully created * @param {String} username * @param {String} password * @param {String} name * @param {String} email * @return {Boolen} account_created */ reg(username, password, name, email) { let form = new formData(); form.append('username', username) form.append('password', password) form.append('firstname', name) form.append('email', email) form.append('seamless_login_enabled', "1") return fetch('https://www.instagram.com/accounts/web_create_ajax/', { 'method': 'post', 'body': form, 'headers': { 'referer': 'https://www.instagram.com/', 'origin': 'https://www.instagram.com', 'user-agent': this.userAgent, 'x-instagram-ajax': '1', 'x-requested-with': 'XMLHttpRequest', 'x-csrftoken': this.csrfToken, cookie: 'csrftoken=' + this.csrfToken } }) .then(res => res.json()) .then(json => { //console.log(json.errors); return json.account_created; }) .catch(() => console.log('Instagram registration failed')) } /** * I did not want to implement this, but I need a stars on github * If you use this library - star this rep https://github.com/yatsenkolesh/instagram-nodejs * Thank you, bro * Follow/unfollow user by id * @param {int} userID * @param {boolean} isUnfollow * @return {object} Promise of fetch request */ follow(userId, isUnfollow) { const headers = { 'referer': 'https://www.instagram.com/', 'origin': 'https://www.instagram.com', 'user-agent': this.userAgent, 'x-instagram-ajax': '1', 'content-type': 'application/json', 'x-requested-with': 'XMLHttpRequest', 'x-csrftoken': undefined, cookie: ' sessionid=' + this.sessionId + '; csrftoken=' + this.csrfToken + '; mid=WPL0LQAEAAGG3XL5-xHXzClnpqA3; rur=ASH; mid=WRN1_AAEAAE07QksztCl3OCnLj8Y;' } return fetch('https://www.instagram.com/web/friendships/' + userId + (isUnfollow == 1 ? '/unfollow' : '/follow'), { 'method': 'post', 'headers': this.getHeaders()//headers }).then(res => { return res }) } /** * @return {Object} default headers */ getHeaders() { return { 'referer': 'https://www.instagram.com/p/BT1ynUvhvaR/?taken-by=yatsenkolesh', 'origin': 'https://www.instagram.com', 'user-agent': this.userAgent, 'x-instagram-ajax': '1', 'x-requested-with': 'XMLHttpRequest', 'x-csrftoken': this.csrfToken, cookie: ' sessionid=' + this.sessionId + '; csrftoken=' + this.csrfToken + ';' } } /** * Return user data by id * @param {Int} id * @return {Object} promise */ getUserDataById(id) { let query = 'ig_user(' + id + '){id,username,external_url,full_name,profile_pic_url,biography,followed_by{count},follows{count},media{count},is_private,is_verified}' let form = new formData(); form.append('q', query) return fetch('https://www.instagram.com/query/', { 'method': 'post', 'body': form, 'headers': this.getHeaders() }).then(res => res.json().then(t => t) ) } /** * When you pass items counter param instagram create pagination * tokens on all iterations and gives on every response end_cursor, which the need to pass on next feed request * * This method return first "items" posts of feed * Coming soon will be opportunity for get part of feed * On testing stage (+- all rights) * If you have a problems - create issue : https://github.com/yatsenkolesh/instagram-nodejs * @param {Int} items (default - 10) * @return {Object} Promise */ getFeed(items, cursor) { items = items ? items : 10; return fetch('https://www.instagram.com/graphql/query/?query_id=17866917712078875&fetch_media_item_count=' + items + '&fetch_media_item_cursor=' + cursor + '&fetch_comment_count=4&fetch_like=10', { headers: this.getHeaders(), }).then(t => // console.log(t) t.json().then(r => r) ) } /** * Simple variable for get next page * @param {Object} json contents from this.getFeed * @return {String} if next page is not exists - false */ getFeedNextPage(json) { let page = json.data.user.edge_web_feed_timeline.page_info return page.has_next_page ? page.end_cursor : false } /** * Attention: postId need transfer only as String (reason int have max value - 2147483647) * @example postID - '1510335854710027921' * @param {String} post id * @return {Object} Promse */ like(postId) { return fetch('https://www.instagram.com/web/likes/' + postId + '/like/', { 'method': 'POST', 'headers': this.getHeaders() }).then(t => t.json().then(r => r) ) } /** * Attention: postId need transfer only as String (reason int have max value - 2147483647) * @example postID - '1510335854710027921' * @param {String} postId * @return {Object} Promse */ unlike(postId) { return fetch('https://www.instagram.com/web/likes/' + postId + '/unlike/', { 'method': 'POST', 'headers': this.getHeaders() }).then(t => t.json().then(r => r) ) } /** * @example url = https://www.instagram.com/p/BT1ynUvhvaR/ * @param {String} url * @return {Object} Promise */ getMediaInfoByUrl(url) { return fetch('https://api.instagram.com/oembed/?url=' + url, { 'headers': this.getHeaders() }).then(t => t.json().then(r => r)) } /** * @example url = https://www.instagram.com/p/BT1ynUvhvaR/ * @param {String} url * @return {Object} Promise */ getMediaIdByUrl(url) { return this.getMediaInfoByUrl(url).then(t => t.media_id.split('_')[0]) } /** * Get media user list on userId with pagination * @param {String} userId * @param {String} cursor (next cursor). Use 0, if you want to get first page * @param {Int} mediaCounter default - 12 * @return {Object} Promise */ getUserMedia(userId, cursor, mediaCounter) { cursor = cursor ? cursor : '0' mediaCounter = mediaCounter ? mediaCounter : 12 let form = new formData() form.append('q', 'ig_user(' + userId + ') { media.after(' + cursor + ', ' + mediaCounter + ') {\ count,\ nodes {\ __typename,\ caption,\ code,\ comments {\ count\ },\ comments_disabled,\ date,\ dimensions {\ height,\ width\ },\ display_src,\ id,\ is_video,\ likes {\ count\ },\ owner {\ id\ },\ thumbnail_src,\ video_views\ },\ page_info\ }\ }') form.append('ref', 'users::show') form.append('query_id', '17849115430193904') // this is static id. May be changed after rebuild, but now actually return fetch('https://www.instagram.com/query/', { headers: this.getHeaders(), method: 'post', body: form }).then(r => r.text().then(t => t)) } /** * End cursor - t.entry_data.TagPage[0].tag.media.page_info['end_cursor'] * Media(nodes) - t.entry_data.TagPage[0].tag.media['nodes'] * @param {String} searchBy - location, hashtag * @param {String} q - location id, or hashtag * @param {String} cursor pagination cursor * @param {Int} mediaCounter * @return {Object} Promise */ searchBy(searchBy, q, cursor, mediaCounter) { if (this.searchTypes.indexOf(searchBy) === false) throw 'search type ' + searchBy + ' is not found' //exclusion for hashtag if not cursor if (searchBy == 'hashtag' && !cursor) { return fetch('https://www.instagram.com/explore/tags/' + q + '/', { headers: this.getHeaders(), }).then(t => t.text().then(r => JSON.parse(r.match(/\<script type=\"text\/javascript\">window\._sharedData \=(.*)\;<\//)[1]))) } let form = new formData() mediaCounter = mediaCounter ? mediaCounter : 12 form.append('q', 'ig_' + searchBy + '(' + q + ') { media.after(' + cursor + ', ' + mediaCounter + ') {\ count,\ nodes {\ __typename,\ caption,\ code,\ comments {\ count\ },\ comments_disabled,\ date,\ dimensions {\ height,\ width\ },\ display_src,\ id,\ is_video,\ likes {\ count\ },\ owner {\ id\ },\ thumbnail_src,\ video_views\ },\ page_info\ }\ }') form.append('ref', 'locations::show') form.append('query_id', '') //empty return fetch('https://www.instagram.com/query/', { headers: this.getHeaders(), method: 'post', body: form }).then(t => t.json().then(r => r)) } /** * Place id path - r.places[0].place.location['pk'], r.places[1].place.location['pk'], ... * Common search returned locations, hashtags and users * @param {String} q * @return {Object} Promise */ commonSearch(q, rankToken) { rankToken = rankToken ? rankToken : '' return fetch('https://www.instagram.com/web/search/topsearch/?context=blended&query=' + q + '&rank_token=' + rankToken, { headers: this.getHeaders() // no required }).then(t => t.json().then(r => r)) } }