├── .gitignore ├── .circleci └── config.yml ├── helpers └── index.js ├── LICENSE ├── package.json ├── test └── index.js ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest 6 | working_directory: ~/repo 7 | 8 | steps: 9 | - checkout 10 | 11 | # Download and cache dependencies 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "package.json" }} 15 | # fallback to using the latest cache if no exact match is found 16 | - v1-dependencies- 17 | - run: npm install 18 | 19 | 20 | - save_cache: 21 | paths: 22 | - node_modules 23 | key: v1-dependencies-{{ checksum "package.json" }} 24 | # run tests! 25 | - run: 26 | name: run tests! 27 | command: | 28 | export USER_NAME=$username 29 | export PASSWORD=$password 30 | npm test 31 | 32 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | media: { 3 | GraphImage: { 4 | id: '2366929737360789025', 5 | pk: 1442533050805297981, 6 | shortcode: 'CDZBg47ss4h', 7 | comments: [{ id: '17844885632304941' }] 8 | } 9 | }, 10 | users: { 11 | Instagram: { 12 | full_name: 'Instagram', 13 | id: '25025320', 14 | pk: 25025320, 15 | username: 'instagram' 16 | }, 17 | Maluma: { 18 | full_name: 'MALUMA', 19 | id: '44059601', 20 | pk: 44059601, 21 | username: 'maluma' 22 | }, 23 | Xenia: { 24 | full_name: 'XENIA', 25 | id: '184028108', 26 | pk: 184028108, 27 | username: 'xenia' 28 | } 29 | }, 30 | 31 | locations: { 32 | Santiago: { 33 | id: '112371848779363', 34 | pk: 26914683, 35 | name: 'Santiago, Chile', 36 | address: '', 37 | city: '', 38 | short_name: 'Santiago', 39 | lng: -70.6667, 40 | lat: -33.45 41 | } 42 | }, 43 | tags: { 44 | dog: { 45 | name: 'dog', 46 | media_count: 151476906, 47 | id: 17843640247048551 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jesús Lobos 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagram-web-api", 3 | "version": "2.2.2", 4 | "description": "Instagram Private Web API client written in JS", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "repository": "jlobos/instagram-web-api", 8 | "author": "Jesus Lobos (https://jlobos.com)", 9 | "scripts": { 10 | "lint": "xo", 11 | "test": "xo && ava" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "lint-staged" 16 | } 17 | }, 18 | "files": [ 19 | "lib" 20 | ], 21 | "prettier": { 22 | "semi": false, 23 | "singleQuote": true 24 | }, 25 | "keywords": [ 26 | "❤️", 27 | "🤳", 28 | "📷", 29 | "instagram", 30 | "private", 31 | "api", 32 | "web", 33 | "upload", 34 | "like", 35 | "comment", 36 | "save", 37 | "follow", 38 | "search", 39 | "ig", 40 | "photo", 41 | "selfie" 42 | ], 43 | "xo": { 44 | "extends": "prettier", 45 | "rules": { 46 | "camelcase": 0, 47 | "no-await-in-loop": 0, 48 | "no-unmodified-loop-condition": 0 49 | } 50 | }, 51 | "lint-staged": { 52 | "*.js": [ 53 | "xo --quiet", 54 | "prettier --single-quote --no-semi --write", 55 | "git add" 56 | ] 57 | }, 58 | "dependencies": { 59 | "is-url": "^1.2.4", 60 | "request": "^2.88.0", 61 | "request-promise-native": "^1.0.8", 62 | "tough-cookie": "^3.0.1", 63 | "useragent-from-seed": "^1.0.1" 64 | }, 65 | "devDependencies": { 66 | "ava": "^2.4.0", 67 | "eslint-config-prettier": "^2.10.0", 68 | "husky": "^3.0.9", 69 | "lint-staged": "^10.2.11", 70 | "prettier": "^1.19.1", 71 | "xo": "^0.20.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import Instagram from '../lib' 3 | import { media, users, locations, tags } from '../helpers' 4 | 5 | const { USER_NAME, PASSWORD } = process.env 6 | 7 | const client = new Instagram({ username: USER_NAME, password: PASSWORD }) 8 | console.log(USER_NAME, PASSWORD) 9 | 10 | let commentId 11 | let nextPageToken 12 | let userId 13 | 14 | test.before(async t => { 15 | const { userId: id } = await client.login() 16 | userId = id 17 | const profile = await client.getProfile() 18 | t.truthy(profile) 19 | }) 20 | 21 | test('getActivity', async t => { 22 | const user = await client.getActivity() 23 | 24 | t.true('activity_feed' in user) 25 | t.true('edge_follow_requests' in user) 26 | }) 27 | 28 | test('getProfile', async t => { 29 | const profile = await client.getProfile() 30 | 31 | t.is(typeof profile, 'object') 32 | }) 33 | 34 | test('getMediaFeedByLocation', async t => { 35 | const { id, name } = await client.getMediaFeedByLocation({ 36 | locationId: locations.Santiago.id 37 | }) 38 | 39 | t.is(id, locations.Santiago.id) 40 | t.is(name, locations.Santiago.name) 41 | }) 42 | 43 | test('getMediaFeedByHashtag', async t => { 44 | const { name } = await client.getMediaFeedByHashtag({ 45 | hashtag: tags.dog.name 46 | }) 47 | t.is(name, tags.dog.name) 48 | }) 49 | 50 | test('locationSearch', async t => { 51 | const venues = await client.locationSearch({ 52 | query: locations.Santiago.name, 53 | latitude: locations.Santiago.lat, 54 | longitude: locations.Santiago.lng 55 | }) 56 | 57 | t.true(Array.isArray(venues)) 58 | }) 59 | 60 | test('getMediaByShortcode', async t => { 61 | const shortcodeMedia = await client.getMediaByShortcode({ 62 | shortcode: media.GraphImage.shortcode 63 | }) 64 | 65 | t.is(shortcodeMedia.__typename, 'GraphImage') 66 | t.is(shortcodeMedia.id, media.GraphImage.id) 67 | }) 68 | 69 | test('getUserByUsername', async t => { 70 | const user = await client.getUserByUsername({ 71 | username: users.Instagram.username 72 | }) 73 | nextPageToken = user.edge_owner_to_timeline_media.page_info.end_cursor 74 | t.is(user.id, users.Instagram.id) 75 | t.is(user.username, users.Instagram.username) 76 | }) 77 | 78 | test.after('getUserByUsername', async t => { 79 | const perPage = 30 80 | const user = await client._getPosts({ 81 | userId: users.Instagram.id, 82 | perPage, 83 | nextPageToken 84 | }) 85 | t.is( 86 | user.edge_owner_to_timeline_media.edges[0].node.owner.id, 87 | users.Instagram.id 88 | ) 89 | t.true(user.edge_owner_to_timeline_media.edges.length === perPage) 90 | }) 91 | 92 | test('getStoryReelFeed', async t => { 93 | const reels = await client.getStoryReelFeed() 94 | 95 | t.true(Array.isArray(reels)) 96 | t.true(reels.length > 0) 97 | }) 98 | 99 | test('getStoryReels', async t => { 100 | const emptyReels = await client.getStoryReels() 101 | t.true(Array.isArray(emptyReels)) 102 | t.true(emptyReels.length === 0) 103 | 104 | const nonEmptyReels = await client.getStoryReels({ reelIds: users.Xenia.id }) 105 | t.true(Array.isArray(nonEmptyReels)) 106 | }) 107 | 108 | test('getStoryItemsByUsername', async t => { 109 | const storyItems = await client.getStoryItemsByUsername({ 110 | username: users.Xenia.username 111 | }) 112 | 113 | t.true(Array.isArray(storyItems)) 114 | }) 115 | 116 | test('getStoryItemsByHashtag', async t => { 117 | const storyItems = await client.getStoryItemsByHashtag({ 118 | hashtag: tags.dog.name 119 | }) 120 | 121 | t.true(Array.isArray(storyItems)) 122 | t.true(storyItems.length > 0) 123 | }) 124 | 125 | test('getStoryItemsByLocation', async t => { 126 | const storyItems = await client.getStoryItemsByLocation({ 127 | locationId: locations.Santiago.id 128 | }) 129 | 130 | t.true(Array.isArray(storyItems)) 131 | t.true(storyItems.length > 0) 132 | }) 133 | 134 | test('getStoryItemsByReel', async t => { 135 | const storyItems = await client.getStoryItemsByReel({ 136 | reelId: users.Maluma.id 137 | }) 138 | 139 | t.true(Array.isArray(storyItems)) 140 | }) 141 | 142 | test('markStoryItemAsSeen', async t => { 143 | const storyItem = ( 144 | await client.getStoryItemsByHashtag({ 145 | hashtag: tags.dog.name 146 | }) 147 | )[0] 148 | 149 | const { status } = await client.markStoryItemAsSeen({ 150 | reelId: storyItem.owner.id, 151 | reelMediaOwnerId: storyItem.owner.id, 152 | reelMediaId: storyItem.id, 153 | reelMediaTakenAt: storyItem.taken_at_timestamp, 154 | viewSeenAt: storyItem.taken_at_timestamp 155 | }) 156 | 157 | t.is(status, 'ok') 158 | }) 159 | 160 | test('getFollowers', async t => { 161 | const followers = await client.getFollowers({ 162 | userId: users.Instagram.id 163 | }) 164 | 165 | t.true('count' in followers) 166 | t.true(Array.isArray(followers.data)) 167 | }) 168 | 169 | test('getFollowings', async t => { 170 | const followings = await client.getFollowings({ 171 | userId: users.Instagram.id 172 | }) 173 | 174 | t.true('count' in followings) 175 | t.true(Array.isArray(followings.data)) 176 | }) 177 | 178 | test('addComment', async t => { 179 | const { status, id, text } = await client.addComment({ 180 | mediaId: media.GraphImage.id, 181 | text: 'test' 182 | }) 183 | commentId = id 184 | 185 | t.is(text, 'test') 186 | t.is(status, 'ok') 187 | }) 188 | 189 | test.after('deleteComment', async t => { 190 | const { status } = await client.deleteComment({ 191 | mediaId: media.GraphImage.id, 192 | commentId 193 | }) 194 | t.is(status, 'ok') 195 | }) 196 | 197 | test('follow', async t => { 198 | const { status } = await client.follow({ userId: users.Instagram.id }) 199 | t.is(status, 'ok') 200 | }) 201 | 202 | test.after('unfollow', async t => { 203 | const { status } = await client.unfollow({ userId: users.Instagram.id }) 204 | t.is(status, 'ok') 205 | }) 206 | 207 | test('block', async t => { 208 | const { status } = await client.block({ userId: users.Maluma.id }) 209 | t.is(status, 'ok') 210 | }) 211 | 212 | test.after('unblock', async t => { 213 | const { status } = await client.unblock({ userId: users.Maluma.id }) 214 | t.is(status, 'ok') 215 | }) 216 | 217 | test('like', async t => { 218 | const { status } = await client.like({ mediaId: media.GraphImage.id }) 219 | t.is(status, 'ok') 220 | }) 221 | 222 | test.after('unlike', async t => { 223 | const { status } = await client.unlike({ mediaId: media.GraphImage.id }) 224 | t.is(status, 'ok') 225 | }) 226 | 227 | test('save', async t => { 228 | const { status } = await client.save({ mediaId: media.GraphImage.id }) 229 | t.is(status, 'ok') 230 | }) 231 | 232 | test.after('unsave', async t => { 233 | const { status } = await client.unsave({ mediaId: media.GraphImage.id }) 234 | t.is(status, 'ok') 235 | }) 236 | 237 | test('search', async t => { 238 | const { status } = await client.search({ query: 'Instagram' }) 239 | t.is(status, 'ok') 240 | }) 241 | 242 | test('getPhotosByUsername', async t => { 243 | const { status } = await client.search({ username: 'Instagram' }) 244 | t.is(status, 'ok') 245 | }) 246 | 247 | test('getPhotosByHashtag', async t => { 248 | const { status } = await client.search({ hashtag: 'Instagram' }) 249 | t.is(status, 'ok') 250 | }) 251 | 252 | test('getPrivateProfilesFollowRequests', async t => { 253 | const { page_name } = await client.getPrivateProfilesFollowRequests() 254 | t.is(page_name, 'current_follow_requests') 255 | }) 256 | 257 | test('getChainsData', async t => { 258 | const response = await client.getChainsData({ userId: users.Maluma.id }) 259 | t.true(Array.isArray(response)) 260 | }) 261 | 262 | test('getMediaComments', async t => { 263 | const response = await client.getMediaComments({ 264 | shortcode: 'BWl6P', 265 | first: 12 266 | }) 267 | t.true(Number.isInteger(response.count)) 268 | t.true(Array.isArray(response.edges)) 269 | t.true(typeof response.page_info === 'object') 270 | }) 271 | 272 | test('getMediaLikes', async t => { 273 | const response = await client.getMediaLikes({ shortcode: 'BWl6P', first: 12 }) 274 | t.true(Number.isInteger(response.count)) 275 | t.true(Array.isArray(response.edges)) 276 | t.true(typeof response.page_info === 'object') 277 | }) 278 | 279 | test('getHome', async t => { 280 | const { status } = await client.getHome( 281 | 'KGEAxpEdUwUrxxoJvxRoQeXFGooSlADHZ8UaDdSWbnOIxxoUUhyciJ7EGlxNlZjaYcUaXTgUM00qyBrgBhUsLezIGqVTlxqausga5W-fVax9xRryaBdN1EnIGvdQFgzxoMgaFoLO7v7xWQA=' 282 | ) 283 | t.is(status, 'ok') 284 | }) 285 | 286 | test('uploadPhoto', async t => { 287 | const { media, status } = await client.uploadPhoto({ 288 | photo: 'https://tecnoblog.net/wp-content/uploads/2020/04/github-capa.jpg', 289 | caption: 'testing', 290 | post: 'feed' 291 | }) 292 | t.true(typeof media.pk !== 'undefined') 293 | t.is(status, 'ok') 294 | }) 295 | 296 | test('deleteMedia', async t => { 297 | const { 298 | edge_owner_to_timeline_media: { edges: images } 299 | } = await client._getPosts({ userId }) 300 | const [firstNode] = images 301 | const imageID = firstNode.node.id 302 | 303 | const { did_delete, status } = await client.deleteMedia({ mediaId: imageID }) 304 | 305 | t.is(did_delete, true) 306 | t.is(status, 'ok') 307 | }) 308 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | NPM version 7 | Build Status 8 | XO code style 9 | 10 |

11 | 12 | ### A Instagram Private Web API client 🤳✨❤️ 13 | 14 | Simple, easy and very complete implementation of the Instagram private web API. 15 | 16 | - Support for all the main functions of [Instagram Web](https://www.instagram.com/) 17 | - Well tested, CI 18 | - All test runs daily 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm install instagram-web-api 24 | ``` 25 | 26 | ## Usage 27 | 28 | Intance `Instagram` and call `login` method; this stores the credentials in memory. 29 | 30 | ```js 31 | const Instagram = require('instagram-web-api') 32 | const { username, password } = process.env 33 | 34 | const client = new Instagram({ username, password }) 35 | 36 | client 37 | .login() 38 | .then(() => { 39 | client 40 | .getProfile() 41 | .then(console.log) 42 | }) 43 | ``` 44 | 45 | Using `async`/`await` in Node >= 8 46 | 47 | ```js 48 | const Instagram = require('instagram-web-api') 49 | const { username, password } = process.env 50 | 51 | const client = new Instagram({ username, password }) 52 | 53 | ;(async () => { 54 | await client.login() 55 | const profile = await client.getProfile() 56 | 57 | console.log(profile) 58 | })() 59 | ``` 60 | 61 | Save cookies to disk by using a `though-cookie` store. 62 | 63 | ```js 64 | // Packages 65 | const Instagram = require('instagram-web-api') 66 | const FileCookieStore = require('tough-cookie-filestore2') 67 | 68 | const { username, password } = process.env // Only required when no cookies are stored yet 69 | 70 | const cookieStore = new FileCookieStore('./cookies.json') 71 | const client = new Instagram({ username, password, cookieStore }) 72 | 73 | ;(async () => { 74 | // URL or path of photo 75 | const photo = 76 | 'https://scontent-scl1-1.cdninstagram.com/t51.2885-15/e35/22430378_307692683052790_5667315385519570944_n.jpg' 77 | 78 | await client.login() 79 | 80 | // Upload Photo to feed or story, just configure 'post' to 'feed' or 'story' 81 | const { media } = await client.uploadPhoto({ photo: photo, caption: 'testing', post: 'feed' }) 82 | console.log(`https://www.instagram.com/p/${media.code}/`) 83 | })() 84 | ``` 85 | 86 | ## API Reference 87 | 88 | * [Instagram](#instagramcredentials-opts) 89 | * [new Instagram({ username, password, cookieStore }, { language, proxy, requestOptions })](#instagramcredentials-opts) 90 | * [.login({ username, password })](#logincredentials) 91 | * [.logout()](#logout) 92 | * [.getHome()](#gethome) 93 | * [.getUserByUsername({ username })](#getuserbyusernameparams) 94 | * [.getFollowers({ userId, first, after })](#getfollowersparams) 95 | * [.getFollowings({ userId, first, after })](#getfollowingsparams) 96 | * [.getActivity()](#getactivity) 97 | * [.getProfile()](#getprofile) 98 | * [.updateProfile({ name, email, username, phoneNumber, gender, biography, website, similarAccountSuggestions })](#updateprofileparams) 99 | * [.changeProfilePhoto({ photo })](#changeprofilephotoparams) 100 | * [.deleteMedia({ mediaId })](#deletemediaparams) 101 | * [.uploadPhoto({ photo, caption, post })](#uploadphotoparams) 102 | * [.getMediaFeedByLocation({ locationId })](#getmediafeedbylocationparams) 103 | * [.getMediaFeedByHashtag({ hashtag })](#getmediafeedbyhashtagparams) 104 | * [.locationSearch({ query, latitude, longitude })](#locationsearchparams) 105 | * [.getMediaByShortcode({ shortcode })](#getmediabyshortcodeparams) 106 | * [.addComment({ mediaId, text })](#addcommentparams) 107 | * [.deleteComment({ mediaId, commentId })](#deletecommentparams) 108 | * [.getChallenge({ challengeUrl })](#getchallengeparams) 109 | * [.updateChallenge({ challengeUrl, choice, securityCode })](#updatechallengeparams) 110 | * [.resetChallenge({ challengeUrl })](#resetchallengeparams) 111 | * [.replayChallenge({ challengeUrl })](#replaychallengeparams) 112 | * [.approve({ userId })](#approveparams) 113 | * [.ignore({ userId })](#ignoreparams) 114 | * [.follow({ userId })](#followparams) 115 | * [.unfollow({ userId })](#unfollowparams) 116 | * [.block({ userId })](#blockparams) 117 | * [.unblock({ userId })](#unblockparams) 118 | * [.like({ mediaId })](#likeparams) 119 | * [.unlike({ mediaId })](#unlikeparams) 120 | * [.save({ mediaId })](#saveparams) 121 | * [.unsave({ mediaId })](#unsaveparams) 122 | * [.search({ query, context })](#searchparams) 123 | * [.getPhotosByHashtag({hashtag, first, after})](#gethastagphotosparams) 124 | * [.getPhotosByUsername({username, first, after})](#getphotosbyusernameparams) 125 | * [.getPrivateProfilesFollowRequests(cursor)](#getPrivateProfilesFollowRequests) 126 | * [.getChainsData({ userId })](#getChainsData) 127 | * [.getMediaLikes({ shortcode, first, after })](#getMediaLikesParams) 128 | * [.getMediaComments({ shortcode, first, after })](#getMediaCommentsParams) 129 | 130 | ### Instagram(credentials, opts) 131 | ```js 132 | const client = new Instagram({ username: '', password: '' }, { language: 'es-CL' }) 133 | ``` 134 | > Initializes the client. 135 | - `credentials` 136 | - `username`: The username of account 137 | - `password`: The password of account 138 | - `cookieStore`: An optional [`though-cookie`](https://www.npmjs.com/package/tough-cookie) cookie storage, which allows for persistent cookies. Default is `undefined` 139 | - `opts` 140 | - `language`: The language of response from API. Default is `en-US` 141 | - `proxy`: `String` of a proxy to tunnel all requests. Default is `undefined` 142 | 143 | ### login(credentials) 144 | ```js 145 | const { username, password, cookies } = await client.login({ username: '', password: '' }) 146 | const { authenticated, user } = await client.login({ username: '', password: '' }) 147 | ``` 148 | 149 | > Login in the account, this method returns `user` (`true` when username is valid) and `authenticated` (`true` when login was successful) 150 | - `credentials` 151 | - `username`: The username of account 152 | - `password`: The password of account 153 | 154 | ### logout() 155 | ```js 156 | await client.logout() 157 | ``` 158 | > Logout in the account. 159 | 160 | ### getHome() 161 | ```js 162 | const feed = await client.getHome('KGEAxpEdUwUrxxoJvxRoQeXFGooSlADHZ8UaDdSWbnOIxxoUUhyciJ7EGlxNlZjaYcUaXTgUM00qyBrgBhUsLezIGqVTlxqausga5W-fVax9xRryaBdN1EnIGvdQFgzxoMgaFoLO7v7xWQA=') 163 | ``` 164 | > Get home feed timeline, media shared by the people you follow. 165 | - `params` 166 | - `end_cursor` (`String`) for pagination 167 | 168 | ### getUserByUsername(params) 169 | ```js 170 | const instagram = await client.getUserByUsername({ username: 'instagram' }) 171 | const me = await client.getUserByUsername({ username: client.credentials.username }) 172 | ``` 173 | > Get user by username, this method not require authentication for public profiles. 174 | - `params` 175 | - `username`: The username of the profile 176 | 177 | ### getFollowers(params) 178 | ```js 179 | const followers = await client.getFollowers({ userId: '1284161654' }) 180 | ``` 181 | > Get followers for given userId. Be aware that the response gets slightly altered for easier usage. 182 | - `params` 183 | - `userId`: The user id 184 | - `first`: Amount of followers to request. Default is `20` 185 | - `after`: Optional `end_cursor` (`String`) for pagination. 186 | 187 | ### getFollowings(params) 188 | ```js 189 | const followings = await client.getFollowings({ userId: '1284161654' }) 190 | ``` 191 | > Get followings for given userId. Be aware that the response gets slightly altered for easier usage. 192 | - `params` 193 | - `userId`: The user id 194 | - `first`: Amount of followings to request. Default is `20` 195 | - `after`: Optional `end_cursor` (`String`) for pagination. 196 | 197 | ### getActivity() 198 | ```js 199 | const activity = await client.getActivity() 200 | ``` 201 | > Get activity of account, news following, liked, etc. 202 | 203 | ### getProfile() 204 | ```js 205 | const profile = await client.getProfile() 206 | ``` 207 | > Get profile the account `first_name`, `last_name`, `email`, `username`, `phone_number`, `gender`, `birthday`, `biography`, `external_url` and `chaining_enabled`. 208 | 209 | ### updateProfile(params) 210 | ```js 211 | await client.updateProfile({ biography: '❤️', website: 'https://jlobos.com/', gender: 1 }) 212 | ``` 213 | > Update profile the account. 214 | - `params` 215 | - `name`: The full name. Default is ` ` 216 | - `email`: The email of account. Default is ` ` 217 | - `username`: The username of account. Default is `client.credentials.username` 218 | - `phoneNumber`: The Phone Number. Default is ` ` 219 | - `gender`: `Number` `1` male, `2` female and `3` not specified 220 | - `biography`: The Bio. Default is ` ` 221 | - `website`: The Website. Default is ` ` 222 | - `similarAccountSuggestions`: `Boolean` Include your account when recommending similar accounts people might want to follow. Default is `true` 223 | 224 | ### changeProfilePhoto(params) 225 | ```js 226 | const fs = require('fs') 227 | 228 | const photo = fs.join(__dirname, 'photo.jpg') 229 | await client.changeProfilePhoto({ photo }) 230 | ``` 231 | > Change the profile photo. 232 | - `params` 233 | - `photo`: A `String` of path file or URL 234 | 235 | ### deleteMedia(params) 236 | ```js 237 | await client.deleteMedia({ mediaId: '1442533050805297981' }) 238 | ``` 239 | > Delete a media, photo, video, etc. by the id. 240 | - `params` 241 | - `mediaId`: The media id 242 | 243 | ### uploadPhoto(params) 244 | ```js 245 | const photo = 'https://scontent-scl1-1.cdninstagram.com/t51.2885-15/e35/16465198_658888867648924_4042368904838774784_n.jpg' 246 | await client.uploadPhoto({ photo, caption: '❤️', post: 'feed' }) 247 | ``` 248 | > Upload a photo to Instagram. Only jpeg images allowed. 249 | - `params` 250 | - `photo`: A `String` of path file or URL 251 | - `caption`: The caption of photo. Default is ` ` 252 | - `post`: The local post, `feed` or `story` 253 | 254 | ### getMediaFeedByLocation(params) 255 | ```js 256 | const location = await client.getMediaFeedByLocation({ locationId: '26914683' }) 257 | ``` 258 | > Get latitude, longitude, top posts, last media, country, city, and more related to the location. 259 | - `params` 260 | - `locationId`: The location id 261 | 262 | ### getMediaFeedByHashtag(params) 263 | ```js 264 | const tag = client.getMediaFeedByHashtag({ hashtag: 'unicorn' }) 265 | ``` 266 | > Explore last media and top posts feed related to a hashtag. 267 | - `params` 268 | - `hashtag`: A hashtag, not including the "#" 269 | 270 | ### locationSearch(params) 271 | ```js 272 | const venues = client.locationSearch({ query: 'chile', latitude: -33.45, longitude: -70.6667 }) 273 | ``` 274 | > Search venues by latitude and longitude. 275 | - `params` 276 | - `latitude`: Latitude 277 | - `longitude`: Longitude 278 | - `query`: A optional location name. Default is ` ` 279 | 280 | ### getMediaByShortcode(params) 281 | ```js 282 | const media = await client.getMediaByShortcode({ shortcode: 'BQE6Cq2AqM9' }) 283 | ``` 284 | > Get data of a media by the Instagram shortcode 285 | - `params` 286 | - `shortcode`: A shortcode 287 | 288 | ### addComment(params) 289 | ```js 290 | await client.addComment({ mediaId: 1442533050805297981, text: 'awesome' }) 291 | ``` 292 | > Add comment to a media item. 293 | - `params` 294 | - `mediaId`: The media id 295 | - `text`: Comment text 296 | - `replyToCommentId`: Optional comment id to which to reply 297 | 298 | ### deleteComment(params) 299 | ```js 300 | await client.deleteComment({ mediaId: '1442533050805297981', commentId: '17848908229146688' }) 301 | ``` 302 | > Delete a comment. 303 | - `params` 304 | - `mediaId`: The media id 305 | - `commentId`: The comment id 306 | 307 | ### getChallenge(params) 308 | ```js 309 | await client.getChallenge({ challengeUrl: '/challenge/1284161654/a1B2c3d4E6/' }) 310 | ``` 311 | > Get information about a challenge. 312 | - `params` 313 | - `challengeUrl`: A `String` with a challenge path 314 | 315 | ### updateChallenge(params) 316 | ```js 317 | const challengeUrl = '/challenge/1284161654/a1B2c3d4E6/' 318 | 319 | await client.updateChallenge({ challengeUrl, choice: 0 }) 320 | await client.updateChallenge({ challengeUrl, securityCode: 123456 }) 321 | ``` 322 | > Request or submit a verification code for the given challenge. 323 | - `params` 324 | - `challengeUrl`: A `String` with a challenge path 325 | - `choice`: `Number` `0` for phone and `1` for email. Default is `` 326 | - `securityCode`: `Number` the received verification code for the challenge. Default is `` 327 | 328 | ### resetChallenge(params) 329 | ```js 330 | await client.resetChallenge({ challengeUrl: '/challenge/1284161654/a1B2c3d4E6/' }) 331 | ``` 332 | > Reset a challenge to start over again. 333 | - `params` 334 | - `challengeUrl`: A `String` with a challenge path 335 | 336 | ### replayChallenge(params) 337 | ```js 338 | await client.replayChallenge({ challengeUrl: '/challenge/1284161654/a1B2c3d4E6/' }) 339 | ``` 340 | > Request a new verification message. 341 | - `params` 342 | - `challengeUrl`: A `String` with a challenge path 343 | 344 | ### approve(params) 345 | ```js 346 | await client.approve({ userId: '1284161654' }) 347 | ``` 348 | > Approve a friendship request. 349 | - `params` 350 | - `userId`: The user id 351 | 352 | ### ignore(params) 353 | ```js 354 | await client.ignore({ userId: '1284161654' }) 355 | ``` 356 | > Reject a friendship request. 357 | - `params` 358 | - `userId`: The user id 359 | 360 | ### follow(params) 361 | ```js 362 | await client.follow({ userId: '1284161654' }) 363 | ``` 364 | > Follow a user. 365 | - `params` 366 | - `userId`: The user id 367 | 368 | ### unfollow(params) 369 | ```js 370 | await client.unfollow({ userId: '1284161654' }) 371 | ``` 372 | > Unfollow a user. 373 | - `params` 374 | - `userId`: The user id 375 | 376 | ### block(params) 377 | ```js 378 | await client.block({ userId: '1284161654' }) 379 | ``` 380 | > Block a user. 381 | - `params` 382 | - `userId`: The user id 383 | 384 | ### unblock(params) 385 | ```js 386 | await client.unblock({ userId: '1284161654' }) 387 | ``` 388 | > Unblock a user. 389 | - `params` 390 | - `userId`: The user id 391 | 392 | ### like(params) 393 | ```js 394 | await client.like({ mediaId: '1442533050805297981' }) 395 | ``` 396 | > Like a media item. 397 | - `params` 398 | - `mediaId`: The media id 399 | 400 | ### unlike(params) 401 | ```js 402 | await client.unlike({ mediaId: '1442533050805297981' }) 403 | ``` 404 | > Unlike a media item. 405 | - `params` 406 | - `mediaId`: The media id 407 | 408 | ### save(params) 409 | ```js 410 | await client.save({ mediaId: '1442533050805297981' }) 411 | ``` 412 | > Save a media item. 413 | - `params` 414 | - `mediaId`: The media id 415 | 416 | ### unsave(params) 417 | ```js 418 | await client.unsave({ mediaId: '1442533050805297981' }) 419 | ``` 420 | > Unsave a media item. 421 | - `params` 422 | - `mediaId`: The media id 423 | 424 | ### search(params) 425 | ```js 426 | await client.search({ query: 'unicorn' }) 427 | ``` 428 | > Search users, places, or hashtags. 429 | - `params` 430 | - `query`: Query 431 | - `context`: The context of search, `hashtag`, `place`, `user` or `blended`. Default is `blended` 432 | 433 | 434 | 435 | ### getPhotosByHashtag(params) 436 | ```js 437 | await client.getPhotosByHashtag({ hashtag: 'unicorn' }) 438 | ``` 439 | > Get photos for hashtag. 440 | - `params` 441 | - `hashtag`: A `String` with a hashtag 442 | - `first`: A `number` of records to return 443 | - `after`: The query cursor `String` for pagination 444 | 445 | ### getPhotosByUsername(params) 446 | ```js 447 | await client.getPhotosByUsername({ username: 'unicorn' }) 448 | ``` 449 | > Gets user photos. 450 | - `params` 451 | - `username`: A `String` with a hashtag 452 | - `first`: A `number` of records to return 453 | - `after`: The query cursor `String` for pagination 454 | 455 | ### getPrivateProfilesFollowRequests 456 | ```js 457 | await client.getPrivateProfilesFollowRequests(cursor) 458 | ``` 459 | 460 | ### getChainsData 461 | ```js 462 | await client.getChainsData({ userId }) 463 | ``` 464 | > This will return the similar accounts, that you see, when you click on the ARROW in a profile. 465 | - `params` 466 | - `userId`: The user id 467 | 468 | ### getMediaLikes(params) 469 | ```js 470 | await client.getMediaLikes({ shortcode: 'B-0000000', first: '49', after: '' }) 471 | ``` 472 | > This will return the media likes. 473 | - `params` 474 | - `shortcode`: The shortcode media like this: https://www.instagram.com/p/B-00000000/, only put shortcode like this : B-000000000 475 | - `first`: A `number` of records to return max is `49` 476 | - `after`: The query cursor `String` for pagination 477 | 478 | ### getMediaComments(params) 479 | ```js 480 | await client.getMediaComments({ shortcode: 'B-0000000', first: '12', after: '' }).catch((error) => { 481 | console.log(error); 482 | }) 483 | .then((response) => { 484 | console.log(response); 485 | }); 486 | 487 | //The query cursor 'after' maybe return an array, if array you need to convert like this: 488 | let pointer = response.page_info.end_cursor; 489 | // this will try to convert array to json stringify 490 | try{ 491 | pointer = JSON.parse(pointer); 492 | pointer = JSON.stringify(pointer); 493 | }catch(e){ 494 | console.log('Pointer is not array!, don't need to be converted!'); 495 | } 496 | 497 | ``` 498 | > This will return the media comments. 499 | - `params` 500 | - `shortcode`: The shortcode media like this: https://www.instagram.com/p/B-00000000/, only put shortcode like this : B-000000000 501 | - `first`: A `number` of records to return max is `49` 502 | - `after`: The query cursor `String` for pagination 503 | 504 | ## License 505 | 506 | MIT © [Jesús Lobos](https://jlobos.com/) 507 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Native 2 | const fs = require('fs') 3 | const crypto = require('crypto') 4 | 5 | // Packages 6 | const request = require('request-promise-native') 7 | const { Cookie } = require('tough-cookie') 8 | const isUrl = require('is-url') 9 | const useragentFromSeed = require('useragent-from-seed') 10 | 11 | const baseUrl = 'https://www.instagram.com' 12 | 13 | class Instagram { 14 | constructor( 15 | { username, password, cookieStore }, 16 | { language, proxy, requestOptions } = {} 17 | ) { 18 | this.credentials = { 19 | username, 20 | password 21 | } 22 | 23 | const jar = request.jar(cookieStore) 24 | jar.setCookie(request.cookie('ig_cb=1'), baseUrl) 25 | const { value: csrftoken } = 26 | jar.getCookies(baseUrl).find(({ key }) => key === 'csrftoken') || {} 27 | 28 | const userAgent = useragentFromSeed(username) 29 | if (requestOptions === undefined) { 30 | requestOptions = {} 31 | } 32 | requestOptions.baseUrl = baseUrl 33 | requestOptions.uri = '' 34 | requestOptions.headers = { 35 | 'User-Agent': userAgent, 36 | 'Accept-Language': language || 'en-US', 37 | 'X-Instagram-AJAX': 1, 38 | 'X-CSRFToken': csrftoken, 39 | 'X-Requested-With': 'XMLHttpRequest', 40 | Referer: baseUrl 41 | } 42 | requestOptions.proxy = proxy 43 | requestOptions.jar = jar 44 | requestOptions.json = true 45 | this.request = request.defaults(requestOptions) 46 | } 47 | 48 | async login({ username, password } = {}, { _sharedData = true } = {}) { 49 | username = username || this.credentials.username 50 | password = password || this.credentials.password 51 | 52 | // Get CSRFToken from cookie before login 53 | let value 54 | await this.request('/', { resolveWithFullResponse: true }).then(res => { 55 | const pattern = new RegExp(/(csrf_token":")\w+/) 56 | const matches = res.toJSON().body.match(pattern) 57 | value = matches[0].substring(13) 58 | }) 59 | 60 | // Provide CSRFToken for login or challenge request 61 | this.request = this.request.defaults({ 62 | headers: { 'X-CSRFToken': value } 63 | }) 64 | 65 | // Temporary work around for https://github.com/jlobos/instagram-web-api/issues/118 66 | const createEncPassword = pwd => { 67 | return `#PWD_INSTAGRAM_BROWSER:0:${Date.now()}:${pwd}` 68 | } 69 | 70 | // Login 71 | const res = await this.request.post('/accounts/login/ajax/', { 72 | resolveWithFullResponse: true, 73 | form: { username, enc_password: createEncPassword(password) } 74 | }) 75 | 76 | if (!res.headers['set-cookie']) { 77 | throw new Error('No cookie') 78 | } 79 | const cookies = res.headers['set-cookie'].map(Cookie.parse) 80 | 81 | // Get CSRFToken after successful login 82 | const { value: csrftoken } = cookies 83 | .find(({ key }) => key === 'csrftoken') 84 | .toJSON() 85 | 86 | // Provide CSRFToken to request 87 | this.request = this.request.defaults({ 88 | headers: { 'X-CSRFToken': csrftoken } 89 | }) 90 | 91 | this.credentials = { 92 | username, 93 | password, 94 | // Add cookies to credentials 95 | cookies: cookies.map(cookie => cookie.toJSON()) 96 | } 97 | 98 | // Provide _sharedData 99 | if (_sharedData) { 100 | this._sharedData = await this._getSharedData() 101 | } 102 | 103 | return res.body 104 | } 105 | 106 | async _getSharedData(url = '/') { 107 | return this.request(url) 108 | .then( 109 | html => html.split('window._sharedData = ')[1].split(';')[0] 110 | ) 111 | .then(_sharedData => JSON.parse(_sharedData)) 112 | } 113 | 114 | async _getGis(path) { 115 | const { rhx_gis } = this._sharedData || (await this._getSharedData(path)) 116 | 117 | return crypto 118 | .createHash('md5') 119 | .update(`${rhx_gis}:${path}`) 120 | .digest('hex') 121 | } 122 | 123 | async logout() { 124 | return this.request('/accounts/logout/ajax/') 125 | } 126 | 127 | async _getHomeData({ queryHash, variables }) { 128 | return this.request('/graphql/query/', { 129 | qs: { 130 | query_hash: queryHash, 131 | variables: JSON.stringify(variables) 132 | } 133 | }).then(data => data) 134 | } 135 | 136 | async getHome(mediaItemCursor) { 137 | return this._getHomeData({ 138 | queryHash: '01b3ccff4136c4adf5e67e1dd7eab68d', 139 | variables: { 140 | fetch_media_item_cursor: mediaItemCursor 141 | } 142 | }) 143 | } 144 | 145 | async getUserByUsername({ username }) { 146 | return this.request({ 147 | uri: `/${username}/?__a=1`, 148 | headers: { 149 | referer: baseUrl + '/' + username + '/', 150 | 'x-instagram-gis': await this._getGis(`/${username}/`) 151 | } 152 | }).then(data => data.graphql.user) 153 | } 154 | 155 | async getStoryReelFeed({ onlyStories = false } = {}) { 156 | return this.request('/graphql/query/', { 157 | qs: { 158 | query_hash: '60b755363b5c230111347a7a4e242001', 159 | variables: JSON.stringify({ 160 | only_stories: onlyStories 161 | }) 162 | } 163 | }) 164 | .then(data => data.data.user.feed_reels_tray.edge_reels_tray_to_reel) 165 | .then(edge_reels_tray_to_reel => edge_reels_tray_to_reel.edges) 166 | .then(edges => edges.map(edge => edge.node)) 167 | } 168 | 169 | async getStoryReels({ 170 | reelIds = [], 171 | tagNames = [], 172 | locationIds = [], 173 | precomposedOverlay = false 174 | } = {}) { 175 | return this.request('/graphql/query/', { 176 | qs: { 177 | query_hash: '297c491471fff978fa2ab83c0673a618', 178 | variables: JSON.stringify({ 179 | reel_ids: reelIds, 180 | tag_names: tagNames, 181 | location_ids: locationIds, 182 | precomposed_overlay: precomposedOverlay 183 | }) 184 | } 185 | }).then(data => data.data.reels_media) 186 | } 187 | 188 | async getUserIdPhotos({ id, first = 12, after = '' } = {}) { 189 | return this.request('/graphql/query/', { 190 | qs: { 191 | query_hash: '6305d415e36c0a5f0abb6daba312f2dd', 192 | variables: JSON.stringify({ 193 | id, 194 | first, 195 | after 196 | }) 197 | } 198 | }).then(data => data.data) 199 | } 200 | 201 | async getPhotosByHashtag({ hashtag, first = 12, after = '' } = {}) { 202 | return this.request('/graphql/query/', { 203 | qs: { 204 | query_hash: 'ded47faa9a1aaded10161a2ff32abb6b', 205 | variables: JSON.stringify({ 206 | tag_name: hashtag, 207 | first, 208 | after 209 | }) 210 | } 211 | }).then(data => data.data) 212 | } 213 | 214 | async getPhotosByUsername({ username, first, after }) { 215 | const user = await this.getUserByUsername({ username }) 216 | return this.getUserIdPhotos({ id: user.id, first, after }) 217 | } 218 | 219 | async getStoryItemsByUsername({ username }) { 220 | const user = await this.getUserByUsername({ username }) 221 | return this.getStoryItemsByReel({ reelId: user.id }) 222 | } 223 | 224 | async getStoryItemsByHashtag({ hashtag }) { 225 | const reels = await this.getStoryReels({ tagNames: [hashtag] }) 226 | if (reels.length === 0) return [] 227 | return reels[0].items 228 | } 229 | 230 | async getStoryItemsByLocation({ locationId }) { 231 | const reels = await this.getStoryReels({ locationIds: [locationId] }) 232 | if (reels.length === 0) return [] 233 | return reels[0].items 234 | } 235 | 236 | async getStoryItemsByReel({ reelId }) { 237 | const reels = await this.getStoryReels({ reelIds: [reelId] }) 238 | if (reels.length === 0) return [] 239 | return reels[0].items 240 | } 241 | 242 | async markStoryItemAsSeen({ 243 | reelMediaId, 244 | reelMediaOwnerId, 245 | reelId, 246 | reelMediaTakenAt, 247 | viewSeenAt 248 | }) { 249 | return this.request.post('/stories/reel/seen', { 250 | form: { 251 | reelMediaId, 252 | reelMediaOwnerId, 253 | reelId, 254 | reelMediaTakenAt, 255 | viewSeenAt 256 | } 257 | }) 258 | } 259 | 260 | async _getFollowData({ fieldName, queryHash, variables }) { 261 | return this.request('/graphql/query/', { 262 | qs: { 263 | query_hash: queryHash, 264 | variables: JSON.stringify(variables) 265 | } 266 | }) 267 | .then(data => data.data.user[fieldName]) 268 | .then(({ count, page_info, edges }) => ({ 269 | count, 270 | page_info, 271 | data: edges.map(edge => edge.node) 272 | })) 273 | } 274 | 275 | async getFollowers({ userId, first = 20, after }) { 276 | return this._getFollowData({ 277 | fieldName: 'edge_followed_by', 278 | queryHash: '37479f2b8209594dde7facb0d904896a', 279 | variables: { 280 | id: userId, 281 | first, 282 | after 283 | } 284 | }) 285 | } 286 | 287 | async getFollowings({ userId, first = 20, after }) { 288 | return this._getFollowData({ 289 | fieldName: 'edge_follow', 290 | queryHash: '58712303d941c6855d4e888c5f0cd22f', 291 | variables: { 292 | id: userId, 293 | first, 294 | after 295 | } 296 | }) 297 | } 298 | 299 | async getChainsData({ userId }) { 300 | return this.request('/graphql/query/', { 301 | qs: { 302 | query_hash: '7c16654f22c819fb63d1183034a5162f', 303 | variables: JSON.stringify({ 304 | user_id: userId, 305 | include_chaining: true, 306 | include_reel: false, 307 | include_suggested_users: false, 308 | include_logged_out_extras: false, 309 | include_highlight_reels: false 310 | }) 311 | } 312 | }) 313 | .then(data => data.data.user.edge_chaining) 314 | .then(({ edges }) => edges.map(edge => edge.node)) 315 | } 316 | 317 | async getActivity() { 318 | return this.request('/accounts/activity/?__a=1').then( 319 | data => data.graphql.user 320 | ) 321 | } 322 | 323 | async getProfile() { 324 | return this.request('/accounts/edit/?__a=1').then(data => data.form_data) 325 | } 326 | 327 | async updateProfile({ 328 | name = '', 329 | email = '', 330 | username, 331 | phoneNumber = '', 332 | gender, 333 | biography = '', 334 | website = '', 335 | similarAccountSuggestions = true 336 | }) { 337 | return this.request.post('/accounts/edit/', { 338 | form: { 339 | first_name: name, 340 | email, 341 | username: username || this.credentials.username, 342 | phone_number: phoneNumber, 343 | gender, 344 | biography, 345 | external_url: website, 346 | chaining_enabled: similarAccountSuggestions 347 | } 348 | }) 349 | } 350 | 351 | async changeProfilePhoto({ photo }) { 352 | return this.request.post('/accounts/web_change_profile_picture/', { 353 | formData: { 354 | profile_pic: isUrl(photo) ? request(photo) : fs.createReadStream(photo) 355 | } 356 | }) 357 | } 358 | 359 | async deleteMedia({ mediaId }) { 360 | return this.request.post(`/create/${mediaId}/delete/`) 361 | } 362 | 363 | async _uploadPhoto({ photo }) { 364 | // Warning! don't change anything bellow. 365 | const uploadId = Date.now() 366 | 367 | let file 368 | 369 | // Needed to new method, if image is from url. 370 | if (isUrl(photo)) { 371 | // Enconding: null is required, only this way a Buffer is returned 372 | file = await request.get({ url: photo, encoding: null }) 373 | } else { 374 | file = await fs.readFileSync(photo) 375 | } 376 | 377 | const ruploadParams = { 378 | media_type: 1, 379 | upload_id: uploadId.toString(), 380 | upload_media_height: 1080, 381 | upload_media_width: 1080, 382 | xsharing_user_ids: JSON.stringify([]), 383 | image_compression: JSON.stringify({ 384 | lib_name: 'moz', 385 | lib_version: '3.1.m', 386 | quality: '80' 387 | }) 388 | } 389 | 390 | const nameEntity = `${uploadId}_0_${Math.random(1000000000, 9999999999)}` 391 | 392 | const headersPhoto = { 393 | 'x-entity-type': 'image/jpeg', 394 | offset: 0, 395 | 'x-entity-name': nameEntity, 396 | 'x-instagram-rupload-params': JSON.stringify(ruploadParams), 397 | 'x-entity-length': file.byteLength, 398 | 'Content-Length': file.byteLength, 399 | 'Content-Type': 'application/octet-stream', 400 | 'x-ig-app-id': `1217981644879628`, 401 | 'Accept-Encoding': 'gzip', 402 | 'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3), 403 | 'X-IG-Connection-Speed': `${Math.random(1000, 3700)}kbps`, 404 | 'X-IG-Bandwidth-Speed-KBPS': '-1.000', 405 | 'X-IG-Bandwidth-TotalBytes-B': '0', 406 | 'X-IG-Bandwidth-TotalTime-MS': '0' 407 | } 408 | 409 | // Json = false, must be important to post work! 410 | let responseUpload = await this.request({ 411 | uri: `/rupload_igphoto/${nameEntity}`, 412 | headers: headersPhoto, 413 | method: 'POST', 414 | json: false, 415 | body: file 416 | }) 417 | 418 | try { 419 | responseUpload = JSON.parse(responseUpload) 420 | 421 | if ('upload_id' in responseUpload) return responseUpload 422 | 423 | throw new Error('Image upload error') 424 | } catch (e) { 425 | throw new Error(`Image upload error: ${e}`) 426 | } 427 | } 428 | 429 | // Upload to story moved to uploadPhoto 430 | // Post: 'feed' or 'story' 431 | async uploadPhoto({ photo, caption = '', post = 'feed' }) { 432 | const dateObj = new Date() 433 | const now = dateObj 434 | .toISOString() 435 | .replace(/T/, ' ') 436 | .replace(/\..+/, ' ') 437 | const offset = dateObj.getTimezoneOffset() 438 | 439 | const responseUpload = await this._uploadPhoto({ photo }) 440 | 441 | return this.request 442 | .post( 443 | `/create/${post === 'feed' ? 'configure/' : 'configure_to_story/'}`, 444 | { 445 | form: { 446 | upload_id: responseUpload.upload_id, 447 | caption, 448 | timezone_offset: offset, 449 | date_time_original: now, 450 | date_time_digitalized: now, 451 | source_type: '4', 452 | edits: { 453 | crop_original_size: [1080, 1080], 454 | crop_center: [0.0, -0.0], 455 | crop_zoom: 1.0 456 | } 457 | } 458 | } 459 | ) 460 | .then(response => response) 461 | } 462 | 463 | async getMediaFeedByLocation({ locationId }) { 464 | return this.request(`/explore/locations/${locationId}/?__a=1`).then( 465 | data => data.graphql.location 466 | ) 467 | } 468 | 469 | async getMediaFeedByHashtag({ hashtag }) { 470 | return this.request(`/explore/tags/${hashtag}/?__a=1`).then( 471 | data => data.graphql.hashtag 472 | ) 473 | } 474 | 475 | async locationSearch({ query, latitude, longitude, distance = 500 }) { 476 | return this.request('/location_search/', { 477 | qs: { search_query: query, latitude, longitude, distance } 478 | }).then(data => data.venues) 479 | } 480 | 481 | async getMediaByShortcode({ shortcode }) { 482 | return this.request(`/p/${shortcode}/?__a=1`).then( 483 | data => data.graphql.shortcode_media 484 | ) 485 | } 486 | 487 | async getMediaComments({ shortcode, first = 12, after = '' }) { 488 | return this.request('/graphql/query/', { 489 | qs: { 490 | query_hash: 'bc3296d1ce80a24b1b6e40b1e72903f5', 491 | variables: JSON.stringify({ shortcode, first, after }) 492 | } 493 | }) 494 | .then(response => response.data.shortcode_media || {}) 495 | .then(media => media.edge_media_to_parent_comment || {}) 496 | .then(({ count = 0, page_info = {}, edges = [] }) => ({ 497 | count, 498 | page_info, 499 | edges 500 | })) 501 | } 502 | 503 | async getMediaLikes({ shortcode, first = 12, after = '' }) { 504 | return this.request('/graphql/query/', { 505 | qs: { 506 | query_hash: 'd5d763b1e2acf209d62d22d184488e57', 507 | variables: JSON.stringify({ 508 | shortcode, 509 | first, 510 | after 511 | }) 512 | } 513 | }) 514 | .then(response => response.data.shortcode_media || {}) 515 | .then(media => media.edge_liked_by || {}) 516 | .then(({ count = 0, page_info = {}, edges = [] }) => ({ 517 | count, 518 | page_info, 519 | edges 520 | })) 521 | } 522 | 523 | async addComment({ mediaId, text, replyToCommentId }) { 524 | return this.request.post(`/web/comments/${mediaId}/add/`, { 525 | form: { comment_text: text, replied_to_comment_id: replyToCommentId } 526 | }) 527 | } 528 | 529 | async deleteComment({ mediaId, commentId }) { 530 | return this.request.post(`/web/comments/${mediaId}/delete/${commentId}/`) 531 | } 532 | 533 | async getChallenge({ challengeUrl }) { 534 | return this.request(`${challengeUrl}?__a=1`) 535 | } 536 | 537 | async _navigateChallenge({ challengeUrl, endpoint, form }) { 538 | const url = endpoint 539 | ? challengeUrl.replace('/challenge/', `/challenge/${endpoint}/`) 540 | : challengeUrl 541 | return this.request.post(url, { 542 | headers: { 543 | Referer: `${baseUrl}${challengeUrl}` 544 | }, 545 | form 546 | }) 547 | } 548 | 549 | async updateChallenge({ challengeUrl, choice, securityCode }) { 550 | const form = securityCode ? { security_code: securityCode } : { choice } 551 | 552 | return this._navigateChallenge({ 553 | challengeUrl, 554 | form 555 | }) 556 | } 557 | 558 | async resetChallenge({ challengeUrl }) { 559 | return this._navigateChallenge({ 560 | challengeUrl, 561 | endpoint: 'reset' 562 | }) 563 | } 564 | 565 | async replayChallenge({ challengeUrl }) { 566 | return this._navigateChallenge({ 567 | challengeUrl, 568 | endpoint: 'replay' 569 | }) 570 | } 571 | 572 | async approve({ userId }) { 573 | return this.request.post(`/web/friendships/${userId}/approve/`) 574 | } 575 | 576 | async ignore({ userId }) { 577 | return this.request.post(`/web/friendships/${userId}/ignore/`) 578 | } 579 | 580 | async follow({ userId }) { 581 | return this.request.post(`/web/friendships/${userId}/follow/`) 582 | } 583 | 584 | async unfollow({ userId }) { 585 | return this.request.post(`/web/friendships/${userId}/unfollow/`) 586 | } 587 | 588 | async block({ userId }) { 589 | return this.request.post(`/web/friendships/${userId}/block/`) 590 | } 591 | 592 | async unblock({ userId }) { 593 | return this.request.post(`/web/friendships/${userId}/unblock/`) 594 | } 595 | 596 | async like({ mediaId }) { 597 | return this.request.post(`/web/likes/${mediaId}/like/`) 598 | } 599 | 600 | async unlike({ mediaId }) { 601 | return this.request.post(`/web/likes/${mediaId}/unlike/`) 602 | } 603 | 604 | async save({ mediaId }) { 605 | return this.request.post(`/web/save/${mediaId}/save/`) 606 | } 607 | 608 | async unsave({ mediaId }) { 609 | return this.request.post(`/web/save/${mediaId}/unsave/`) 610 | } 611 | 612 | async search({ query, context = 'blended' }) { 613 | return this.request('/web/search/topsearch/', { qs: { query, context } }) 614 | } 615 | 616 | async _getPosts({ userId, perPage = 12, nextPageToken }) { 617 | const variables = JSON.stringify({ 618 | id: userId, 619 | first: perPage, 620 | after: nextPageToken 621 | }) 622 | const options = { 623 | qs: { 624 | query_hash: '42323d64886122307be10013ad2dcc44', 625 | variables 626 | } 627 | } 628 | 629 | return this.request 630 | .get('/graphql/query/', options) 631 | .then(response => response.data.user) 632 | } 633 | 634 | async getPrivateProfilesFollowRequests(cursor) { 635 | const cursorParam = cursor ? `&cursor=${cursor}` : '' 636 | return this.request( 637 | `accounts/access_tool/current_follow_requests?__a=1${cursorParam}` 638 | ) 639 | } 640 | } 641 | 642 | module.exports = Instagram 643 | --------------------------------------------------------------------------------