├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .idea ├── .gitignore ├── jsLibraryMappings.xml ├── modules.xml ├── user-instagram-reborn.iml └── vcs.xml ├── README.md ├── app ├── data.json └── services │ ├── AspectRatioCalculator.js │ ├── BasicRequestHandler.js │ ├── CacheStorage.js │ ├── CookieReader.js │ └── InstagramHelper.js ├── babel.config.json ├── index.js ├── jest.config.json ├── package-lock.json ├── package.json ├── src └── Instagram │ ├── Application │ ├── AuthenticationHandler.js │ ├── GetPostDataHandler.js │ ├── GetUserDataHandler.js │ └── Query │ │ ├── AuthenticationQuery.js │ │ ├── GetPostDataQuery.js │ │ └── GetUserDataQuery.js │ ├── Domain │ ├── ChildMedia.js │ ├── Comment.js │ ├── CommentOwner.js │ ├── Dimensions.js │ ├── Location.js │ ├── Media.js │ ├── MediaType.js │ ├── MediaVariant.js │ ├── Owner.js │ ├── Post.js │ ├── Session.js │ ├── TaggedUser.js │ ├── User.js │ └── errors │ │ ├── InstagramAuthenticationError.js │ │ ├── InstagramGenericError.js │ │ ├── NoSessionError.js │ │ ├── PostNotFoundError.js │ │ ├── RateLimitError.js │ │ ├── TooManyLoginsError.js │ │ └── UserNotFoundError.js │ └── Infrastructure │ └── InstagramRepository.js └── tests └── app └── services ├── aspectRatioCalculator.spec.js └── cookieReader.spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "@babel/eslint-parser", 4 | "parserOptions": { 5 | "babelOptions": { 6 | "configFile": "./babel.config.json" 7 | } 8 | }, 9 | "rules": { 10 | "lines-between-class-members": "off", 11 | "comma-dangle": "off", 12 | "max-len": "off", 13 | "no-trailing-spaces": "off", 14 | "dot-notation": "off", 15 | "indent": "off", 16 | "quote-props": "off", 17 | "no-shadow": "off", 18 | "prefer-destructuring": "off", 19 | "no-prototype-builtins": "off", 20 | "object-shorthand": "off", 21 | "consistent-return": "off" 22 | }, 23 | "env": { 24 | "jest": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master, '2.2' ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [14.16.1] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: npm install 26 | - name: Running ESLint 27 | run: npm run eslint 28 | - name: Run unit tests 29 | run: npm run test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/user-instagram-reborn.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # User Instagram - DEPRECATED 2 | 3 | [![Downloads](https://img.shields.io/npm/dt/user-instagram)](https://img.shields.io/david/EdouardCourty/user-instagram) 4 | [![Node.js CI](https://github.com/EdouardCourty/user-instagram/actions/workflows/node.js.yml/badge.svg)](https://github.com/EdouardCourty/user-instagram/actions/workflows/node.js.yml) 5 | 6 | ## The end 7 | 8 | Thursday 07 2022. 9 | Instagram's API & Authentication changed a lot and I don't have the time to update and reverse-engineer everything again. 10 | I have no choice but to deprecate this module. 11 | If you want to become the official maintainer, feel free to open an issue and we'll get in touch! 12 | 13 | Thanks to the thousands of people that used this module through the years! 14 | Big love from Paris! 15 | 16 | ## Usage 17 | 18 | Use this module in your projet by installing it with `npm install user-instagram`. 19 | 20 | Here is a quick example or usage: 21 | ```javascript 22 | const instagram = require('user-instagram'); 23 | 24 | await instagram.authenticate('my_instagram_username', 'my_instagram_password'); 25 | 26 | // Fetching a user 27 | instagram.getUserData('edouard_courty').then(userData => { 28 | // Do whatever you need to to with this data 29 | console.log(`My username is ${userData.getUsername()}.`); 30 | }) 31 | 32 | // Fetching a post 33 | instagram.getPostData('CUc7tBPFXvP').then(postData => { 34 | // Do whatever you need to to with this data 35 | console.log(`The caption of this post is ${postData.getCaption()}.`); 36 | }) 37 | ``` 38 | 39 | ## Documentation 40 | 41 | In the previous versions of `user-instagram`, only a small amount of requests could be sent every day without getting rate-mimited. 42 | Couple of issues were submitted about this problem and it's the main reason why I decided to refactor this module, and add an authentication method to it. 43 | 44 | ### Authentication 45 | 46 | The `authenticate` method takes two mandatory parameters: `username` and `password`. 47 | A good way to keep these strings safe is storing them in an uncommited file in your repo like some `instagram_config.json` file structured like the following: 48 | ```json 49 | { 50 | "username": "your username here", 51 | "password": "your password here" 52 | } 53 | ``` 54 | Then use it like this: 55 | ```javascript 56 | const instagram = require('user-instagram'); 57 | const {username, password} = require('instagram_config.json') 58 | 59 | await instagram.authenticate(username, password); 60 | ``` 61 | 62 | ### Getting a user's data 63 | 64 | When logged in, you can request the data of any public user you want, plus the private users that your account follows. 65 | The `getUserData` method takes only one parameter: the username of the user to be fetched. 66 | 67 | This method will return a promise holding a `User` class, containing getters for all the interesting properties of this class. 68 | 69 | #### Available user properties 70 | 71 | All the boolean values are accessed with the following methods: (the function names should be self explanatory of their return value) 72 | - `isVerified()` 73 | - `isPrivate()` 74 | - `isBusinessAccount()` 75 | - `isProfessionalAccount()` 76 | - `hasClips()` 77 | - `hasArEffect()` 78 | - `hasChannel()` 79 | - `hasGuides()` 80 | - `isHidingLikesAndViewsCount()` 81 | - `hasJoinedRecently()` 82 | 83 | All the non-boolean values are accessed with the following methods: 84 | - `getUsername()` 85 | - `getBiography()` 86 | - `getPublicationsCount()` 87 | - `getFollowersCount()` 88 | - `getExternalUrl()` 89 | - `getFollowingCount()` 90 | - `getFullName()` 91 | - `getHighlightsReelsCount()` 92 | - `getId()` 93 | - `getBusinessAddressJson()` 94 | - `getBusinessContactMethod()` 95 | - `getBusinessEmail()` 96 | - `getBusinessPhoneNumber()` 97 | - `getBusinessCategoryName()` 98 | - `getOverallCategoryName()` 99 | - `getCategoryEnum()` 100 | - `getProfilePicture()` 101 | - `getHdProfilePicture()` 102 | - `getPronouns()` 103 | - `getMedias()` 104 | 105 | ### Getting a post's data 106 | 107 | When logged in, you can request the data of any public post you want, plus the posts of the private accounts that your account follows. 108 | The `getPostData` method takes only one parameter: the shortcode of the post to be fetched. 109 | 110 | This method will return a promise holding a `Post` class, containing getters for all the interesting properties of this class. 111 | 112 | #### Available post properties 113 | 114 | All the boolean values are accessed with the following methods: (the function names should be self explanatory of their return value) 115 | - `isVideo()` 116 | - `areCommentsDisabled()` 117 | - `areLikesAndViewsCountDisabled()` 118 | - `isPaidPartnership()` 119 | - `isAd()` 120 | - `hasAudio()` 121 | 122 | All the non-boolean values are accessed with the following methods: 123 | - `getId()` 124 | - `getType()` 125 | - `getShortcode()` 126 | - `getDimensions()` 127 | - `getDisplayUrl()` 128 | - `getVariants()` 129 | - `getAccessibilityCaption()` 130 | - `getTaggedUsers()` 131 | - `getCaption()` 132 | - `getCommentsCount()` 133 | - `getComments()` 134 | - `getDate()` 135 | - `getLikesCount()` 136 | - `getLocation()` 137 | - `getOwner()` 138 | - `getChildren()` 139 | - `getVideoViewsCount()` 140 | - `getVideoPlaysCount()` 141 | 142 | #### Generic properties shared across the module 143 | 144 | Each Media from the `getMedias()` in the `User` class method is a `Media` class that has these getters: 145 | - `getType()` 146 | - `getId()` 147 | - `getShortcode()` 148 | - `getCaption()` 149 | - `getDimensions()` 150 | - `getDisplayUrl()` 151 | - `getTaggedUsers()` 152 | - `isVideo()` 153 | - `getAccessibilityCaption()` 154 | - `areCommentsDisabled()` 155 | - `getCommentsCount()` 156 | - `getLikesCount()` 157 | - `getTimestamp()` 158 | - `getLocation()` 159 | - `getChildren()` 160 | - `hasAudio()` 161 | - `getViewsCount()` 162 | - `getVideoUrl()` 163 | 164 | Every `TaggerUser` from `getTaggedUsers()` in a `Post` or a `User.getMedias()` hold the following getters: 165 | - `getTagXPosition()` 166 | - `getTagYPosition()` 167 | - `getFullName()` 168 | - `getId()` 169 | - `isVerified()` 170 | - `getProfilePictureUrl()` 171 | - `getUsername()` 172 | 173 | Every dimension value from `getDimensions()` from a `Media` or a `Post` is a `Dimension` class with a bult-in aspect-ratio calculator: 174 | - `getHeight()` 175 | - `getWidth()` 176 | - `getAspectRatio()` 177 | 178 | © Edouard Courty - 2021 179 | -------------------------------------------------------------------------------- /app/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "usernameUrlTemplate": "https://instagram.com/%USERNAME%/?__a=1", 3 | "postUrlTemplate": "https://instagram.com/p/%SHORTCODE%/?__a=1", 4 | "headers": { 5 | "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36", 6 | "sec-fetch-site": "none", 7 | "sec-fetch-dest": "document", 8 | "sec-fetch-mode": "navigate", 9 | "sec-fetch-user": "?1", 10 | "sec-ch-ua-mobile": "?0", 11 | "sec-ch-ua-platform": "\"Linux\"", 12 | "sec-ch-ua": "\"Google Chrome\";v=\"93\", \" Not;A Brand\";v=\"99\", \"Chromium\";v=\"93\"", 13 | "accept-encoding": "gzip, deflate, br" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/services/AspectRatioCalculator.js: -------------------------------------------------------------------------------- 1 | class AspectRatioCalculator { 2 | /** 3 | * @param {number} a 4 | * @param {number} b 5 | * @returns {number} 6 | */ 7 | static #gcd = (a, b) => (b === 0 ? a : this.#gcd(b, a % b)); 8 | /** 9 | * @param {number} a 10 | * @param {number} b 11 | * @returns {number[]} 12 | */ 13 | static #highestFirst = (a, b) => (a < b ? [b, a] : [a, b]); 14 | /** 15 | * @param {number} h 16 | * @param {number} w 17 | * @param {number} divisor 18 | * @param {string|null} separator 19 | * @returns {string} 20 | */ 21 | static #format = (h, w, divisor, separator) => `${h / divisor}${separator}${w / divisor}`; 22 | 23 | /** 24 | * @param {number} height 25 | * @param {number} width 26 | * @param {string} separator 27 | * @returns {string} 28 | */ 29 | static getAspectRatio(height, width, separator = ':') { 30 | const [h, w] = this.#highestFirst(height, width); 31 | const divisor = this.#gcd(h, w); 32 | return this.#format(h, w, divisor, separator); 33 | } 34 | } 35 | 36 | module.exports = AspectRatioCalculator; 37 | -------------------------------------------------------------------------------- /app/services/BasicRequestHandler.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const applicationData = require('../data.json'); 4 | 5 | const CacheStorage = require('./CacheStorage'); 6 | const NoSessionError = require('../../src/Instagram/Domain/errors/NoSessionError'); 7 | const InstagramRepository = require('../../src/Instagram/Infrastructure/InstagramRepository'); 8 | 9 | class BasicRequestHandler { 10 | static async handle(queryUrl) { 11 | if (!CacheStorage.get().has('session')) { 12 | throw new NoSessionError(); 13 | } 14 | 15 | /** @type Session */ 16 | let session = CacheStorage.get().get('session'); 17 | 18 | if (session.getExpiryDate() < new Date()) { 19 | await InstagramRepository.authenticate(); 20 | session = CacheStorage.get().get('session'); 21 | } 22 | 23 | return new Promise((resolve, reject) => { 24 | const headers = { 25 | ...applicationData.headers, 26 | cookie: `sessionid=${session.getId()}` 27 | }; 28 | axios.get(queryUrl, { 29 | headers: headers 30 | }).then(resolve).catch(reject); 31 | }); 32 | } 33 | } 34 | 35 | module.exports = BasicRequestHandler; 36 | -------------------------------------------------------------------------------- /app/services/CacheStorage.js: -------------------------------------------------------------------------------- 1 | class CacheStorage { 2 | static #pool = new Map(); 3 | 4 | static get = () => this.#pool; 5 | } 6 | 7 | module.exports = CacheStorage; 8 | -------------------------------------------------------------------------------- /app/services/CookieReader.js: -------------------------------------------------------------------------------- 1 | class CookieReader { 2 | /** 3 | * @param {string} cookie 4 | * @param {string} cookieKey 5 | * @returns Promise 6 | */ 7 | static read(cookie, cookieKey) { 8 | const key = cookieKey.trim(); 9 | return new Promise((resolve, reject) => { 10 | cookie 11 | .split('=') 12 | .map((splitted) => splitted.split(';')) 13 | .flat(1) 14 | .forEach((arrayValue, index, array) => { 15 | const value = arrayValue.trim(); 16 | if (index > 0 && array[index - 1].trim() === key) { 17 | return resolve(value); 18 | } 19 | }); 20 | return reject(); 21 | }); 22 | } 23 | } 24 | 25 | module.exports = CookieReader; 26 | -------------------------------------------------------------------------------- /app/services/InstagramHelper.js: -------------------------------------------------------------------------------- 1 | class InstagramHelper { 2 | static BASE_URL = 'https://www.instagram.com/'; 3 | static AUTH_URL = 'https://www.instagram.com/accounts/login/ajax/'; 4 | static DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'; 5 | 6 | static #ENCRYPTED_PASSWORD_TEMPLATE = '#PWD_INSTAGRAM_BROWSER:0:%TIME%:%PASSWORD%' 7 | 8 | static storage = new Map(); 9 | 10 | /** 11 | * @param {string} password 12 | * @returns {string} 13 | */ 14 | static getEncryptedPasswordFromPassword(password) { 15 | const currentDate = new Date(); 16 | const unixTimestamp = Math.round(currentDate.getTime() / 1000); 17 | return this.#ENCRYPTED_PASSWORD_TEMPLATE 18 | .replace('%TIME%', unixTimestamp.toString()) 19 | .replace('%PASSWORD%', password); 20 | } 21 | } 22 | 23 | module.exports = InstagramHelper; 24 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-class-properties", 4 | "@babel/plugin-proposal-private-methods" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const GetUserDataHandler = require('./src/Instagram/Application/GetUserDataHandler'); 2 | const GetPostDataHandler = require('./src/Instagram/Application/GetPostDataHandler'); 3 | const AuthenticationHandler = require('./src/Instagram/Application/AuthenticationHandler'); 4 | 5 | const GetUserDataQuery = require('./src/Instagram/Application/Query/GetUserDataQuery'); 6 | const GetPostDataQuery = require('./src/Instagram/Application/Query/GetPostDataQuery'); 7 | const AuthenticationQuery = require('./src/Instagram/Application/Query/AuthenticationQuery'); 8 | 9 | module.exports = { 10 | /** 11 | * @param {string} username 12 | * @returns Promise 13 | */ 14 | getUserData: (username) => GetUserDataHandler.handle(new GetUserDataQuery(username)), 15 | /** 16 | * @param {string} shortCode 17 | * @returns {Promise} 18 | */ 19 | getPostData: (shortCode) => GetPostDataHandler.handle(new GetPostDataQuery(shortCode)), 20 | /** 21 | * @param {string} username 22 | * @param {string} password 23 | * @returns {Promise} 24 | */ 25 | authenticate: (username, password) => AuthenticationHandler.handle(new AuthenticationQuery(username, password)) 26 | }; 27 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "transform": {} 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-instagram", 3 | "version": "3.0.2", 4 | "description": "Get user & post data without authentication", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/EdouardCourty/user-instagram.git" 9 | }, 10 | "type": "commonjs", 11 | "keywords": [ 12 | "Instagram", 13 | "Instagram API", 14 | "Instagram user", 15 | "Scrapping" 16 | ], 17 | "author": "Edouard Courty", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/EdouardCourty/user-instagram/issues" 21 | }, 22 | "homepage": "https://github.com/EdouardCourty/user-instagram#readme", 23 | "dependencies": { 24 | "axios": "^0.21.1", 25 | "cookie-parser": "^1.4.5" 26 | }, 27 | "scripts": { 28 | "eslint": "eslint .", 29 | "test": "node --experimental-vm-modules node_modules/.bin/jest tests" 30 | }, 31 | "devDependencies": { 32 | "@babel/eslint-parser": "^7.14.5", 33 | "@babel/plugin-proposal-class-properties": "^7.14.5", 34 | "@babel/plugin-proposal-private-methods": "^7.14.5", 35 | "eslint": "^7.32.0", 36 | "eslint-config-airbnb": "^18.2.1", 37 | "eslint-plugin-import": "^2.25.2", 38 | "eslint-plugin-jsx-a11y": "^6.4.1", 39 | "eslint-plugin-react": "^7.25.1", 40 | "jest": "^27.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Instagram/Application/AuthenticationHandler.js: -------------------------------------------------------------------------------- 1 | const InstagramRepository = require('../Infrastructure/InstagramRepository'); 2 | 3 | const CacheStorage = require('../../../app/services/CacheStorage'); 4 | 5 | class AuthenticationHandler { 6 | /** 7 | * @param {AuthenticationQuery} authenticationQuery 8 | */ 9 | static handle(authenticationQuery) { 10 | CacheStorage.get().set('username', authenticationQuery.getUsername()); 11 | CacheStorage.get().set('password', authenticationQuery.getPassword()); 12 | return InstagramRepository.authenticate(); 13 | } 14 | } 15 | 16 | module.exports = AuthenticationHandler; 17 | -------------------------------------------------------------------------------- /src/Instagram/Application/GetPostDataHandler.js: -------------------------------------------------------------------------------- 1 | const InstagramRepository = require('../Infrastructure/InstagramRepository'); 2 | 3 | class GetPostDataHandler { 4 | /** 5 | * @param {GetPostDataQuery} getPostDataQuery 6 | * @returns Promise 7 | */ 8 | static handle(getPostDataQuery) { 9 | return InstagramRepository.getPost(getPostDataQuery.getShortCode()); 10 | } 11 | } 12 | 13 | module.exports = GetPostDataHandler; 14 | -------------------------------------------------------------------------------- /src/Instagram/Application/GetUserDataHandler.js: -------------------------------------------------------------------------------- 1 | const InstagramRepository = require('../Infrastructure/InstagramRepository'); 2 | 3 | class GetUserDataHandler { 4 | /** 5 | * @param {GetUserDataQuery} getUserDataQuery 6 | * @returns Promise 7 | */ 8 | static handle(getUserDataQuery) { 9 | return InstagramRepository.getUser(getUserDataQuery.getUserName()); 10 | } 11 | } 12 | 13 | module.exports = GetUserDataHandler; 14 | -------------------------------------------------------------------------------- /src/Instagram/Application/Query/AuthenticationQuery.js: -------------------------------------------------------------------------------- 1 | class AuthenticationQuery { 2 | /** @type string */ 3 | #username; 4 | /** @type string */ 5 | #password; 6 | 7 | /** 8 | * @param {string} username 9 | * @param {string} password 10 | */ 11 | constructor(username, password) { 12 | if (typeof username !== 'string') { 13 | throw new TypeError('The username has to be a string.'); 14 | } else if (typeof password !== 'string') { 15 | throw new TypeError('The password has to be a string.'); 16 | } 17 | this.#username = username; 18 | this.#password = password; 19 | } 20 | 21 | /** @returns string */ 22 | getUsername = () => this.#username; 23 | /** @returns string */ 24 | getPassword = () => this.#password; 25 | } 26 | 27 | module.exports = AuthenticationQuery; 28 | -------------------------------------------------------------------------------- /src/Instagram/Application/Query/GetPostDataQuery.js: -------------------------------------------------------------------------------- 1 | class GetPostDataQuery { 2 | /** @type string */ 3 | #shortCode; 4 | 5 | /** 6 | * @param {string} shortCode 7 | */ 8 | constructor(shortCode) { 9 | if (typeof shortCode !== 'string') { 10 | throw new TypeError('The sortcode has to be a string.'); 11 | } 12 | this.#shortCode = shortCode; 13 | } 14 | 15 | /** @returns string */ 16 | getShortCode = () => this.#shortCode; 17 | } 18 | 19 | module.exports = GetPostDataQuery; 20 | -------------------------------------------------------------------------------- /src/Instagram/Application/Query/GetUserDataQuery.js: -------------------------------------------------------------------------------- 1 | class GetUserDataQuery { 2 | /** @type string */ 3 | #username; 4 | 5 | /** 6 | * @param {string} username 7 | */ 8 | constructor(username) { 9 | if (typeof username !== 'string') { 10 | throw TypeError('The username has to be a string.'); 11 | } 12 | this.#username = username; 13 | } 14 | 15 | /** @returns string */ 16 | getUserName = () => this.#username; 17 | } 18 | 19 | module.exports = GetUserDataQuery; 20 | -------------------------------------------------------------------------------- /src/Instagram/Domain/ChildMedia.js: -------------------------------------------------------------------------------- 1 | class ChildMedia { 2 | /** @type MediaType */ 3 | #type; 4 | /** @type string */ 5 | #id; 6 | /** @type string */ 7 | #shortcode; 8 | /** @type Dimensions */ 9 | #dimensions; 10 | /** @type string */ 11 | #displayUrl; 12 | /** @type TaggedUser[] */ 13 | #taggedUsers; 14 | /** @type string */ 15 | #accessibilityCaption; 16 | 17 | constructor(type, id, shortcode, dimensions, displayUrl, taggedUsers, accessibilityCaption) { 18 | this.#type = type; 19 | this.#id = id; 20 | this.#shortcode = shortcode; 21 | this.#dimensions = dimensions; 22 | this.#displayUrl = displayUrl; 23 | this.#taggedUsers = taggedUsers; 24 | this.#accessibilityCaption = accessibilityCaption; 25 | } 26 | 27 | getType = () => this.#type.getValue(); 28 | getId = () => this.#id; 29 | getShortcode = () => this.#shortcode; 30 | getDimensions = () => this.#dimensions; 31 | getDisplayUrl = () => this.#displayUrl; 32 | getTaggedUsers = () => this.#taggedUsers; 33 | getAccessibilityCaption = () => this.#accessibilityCaption; 34 | } 35 | 36 | module.exports = ChildMedia; 37 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Comment.js: -------------------------------------------------------------------------------- 1 | class Comment { 2 | /** @type string */ 3 | #id; 4 | /** @type string */ 5 | #text; 6 | /** @type Date */ 7 | #date; 8 | /** @type CommentOwner */ 9 | #owner; 10 | /** @type number */ 11 | #likesCount; 12 | /** @type boolean */ 13 | #hasResponses; 14 | /** @type Comment[] */ 15 | #responses; 16 | 17 | constructor(id, text, date, owner, likesCount, hasResponses, responses) { 18 | this.#id = id; 19 | this.#text = text; 20 | this.#date = date; 21 | this.#owner = owner; 22 | this.#likesCount = likesCount; 23 | this.#hasResponses = hasResponses; 24 | this.#responses = responses; 25 | } 26 | 27 | getId = () => this.#id; 28 | getText = () => this.#text; 29 | getDate = () => this.#date; 30 | getOwner = () => this.#owner; 31 | getLikesCount = () => this.#likesCount; 32 | hasResponses = () => this.#hasResponses; 33 | getResponses = () => this.#responses; 34 | } 35 | 36 | module.exports = Comment; 37 | -------------------------------------------------------------------------------- /src/Instagram/Domain/CommentOwner.js: -------------------------------------------------------------------------------- 1 | class CommentOwner { 2 | /** @type string */ 3 | #id; 4 | /** @type boolean */ 5 | #isVerified; 6 | /** @type string */ 7 | #profilePicUrl; 8 | /** @type string */ 9 | #username; 10 | 11 | constructor(id, isVerified, profilePicUrl, username) { 12 | this.#id = id; 13 | this.#isVerified = isVerified; 14 | this.#profilePicUrl = profilePicUrl; 15 | this.#username = username; 16 | } 17 | 18 | getId = () => this.#id; 19 | isVerified = () => this.#isVerified; 20 | getProfilePicUrl = () => this.#profilePicUrl; 21 | getUsername = () => this.#username; 22 | } 23 | 24 | module.exports = CommentOwner; 25 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Dimensions.js: -------------------------------------------------------------------------------- 1 | const AspectRatioCalculator = require('../../../app/services/AspectRatioCalculator'); 2 | 3 | class Dimensions { 4 | /** @type number */ 5 | #height; 6 | /** @type number */ 7 | #width; 8 | 9 | constructor(height, width) { 10 | this.#height = height; 11 | this.#width = width; 12 | } 13 | 14 | getHeight = () => this.#height; 15 | getWidth = () => this.#width; 16 | /** 17 | * @param {string|null} separator 18 | * @returns {string} 19 | */ 20 | getAspectRatio = (separator = ':') => AspectRatioCalculator.getAspectRatio(this.#height, this.#width, separator); 21 | } 22 | 23 | module.exports = Dimensions; 24 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Location.js: -------------------------------------------------------------------------------- 1 | class Location { 2 | /** @type string */ 3 | #id; 4 | /** @type boolean */ 5 | #hasPublicPage; 6 | /** @type string */ 7 | #name; 8 | /** @type string */ 9 | #slug; 10 | 11 | constructor(id, hasPublicPage, name, slug) { 12 | this.#id = id; 13 | this.#hasPublicPage = hasPublicPage; 14 | this.#name = name; 15 | this.#slug = slug; 16 | } 17 | 18 | getId = () => this.#id; 19 | hasPublicPage = () => this.#hasPublicPage; 20 | getName = () => this.#name; 21 | getSlug = () => this.#slug; 22 | } 23 | 24 | module.exports = Location; 25 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Media.js: -------------------------------------------------------------------------------- 1 | class Media { 2 | /** @type MediaType */ 3 | #type; 4 | /** @type string */ 5 | #id; 6 | /** @type string */ 7 | #shortcode; 8 | /** @type string */ 9 | #caption; 10 | /** @type Dimensions */ 11 | #dimensions; 12 | /** @type string */ 13 | #displayUrl; 14 | /** @type TaggedUser[] */ 15 | #taggedUsers; 16 | /** @type boolean */ 17 | #isVideo; 18 | /** @type string */ 19 | #accessibilityCaption; 20 | /** @type boolean */ 21 | #commentsDisabled; 22 | /** @type number */ 23 | #commentsCount; 24 | /** @type number */ 25 | #likesCount; 26 | /** @type number */ 27 | #timestamp; 28 | /** @type Location */ 29 | #location; 30 | /** @type ChildPicture[] */ 31 | #children; 32 | /** @type boolean */ 33 | #hasAudio; 34 | /** @type number|null */ 35 | #viewsCount; 36 | /** @type string|null */ 37 | #videoUrl; 38 | 39 | constructor(type, id, shortcode, caption, dimensions, displayUrl, taggedUsers, isVideo, accessibilityCaption, commentsDisabled, commentsCount, likesCount, timestamp, location, children, hasAudio = false, viewsCount = 0, videoUrl = null) { 40 | this.#type = type; 41 | this.#id = id; 42 | this.#shortcode = shortcode; 43 | this.#caption = caption; 44 | this.#dimensions = dimensions; 45 | this.#displayUrl = displayUrl; 46 | this.#taggedUsers = taggedUsers; 47 | this.#isVideo = isVideo; 48 | this.#accessibilityCaption = accessibilityCaption; 49 | this.#commentsDisabled = commentsDisabled; 50 | this.#commentsCount = commentsCount; 51 | this.#likesCount = likesCount; 52 | this.#timestamp = timestamp; 53 | this.#location = location; 54 | this.#children = children; 55 | this.#hasAudio = hasAudio; 56 | this.#viewsCount = viewsCount; 57 | this.#videoUrl = videoUrl; 58 | } 59 | 60 | getType = () => this.#type.getValue(); 61 | getId = () => this.#id; 62 | getShortcode = () => this.#shortcode; 63 | getCaption = () => this.#caption; 64 | getDimensions = () => this.#dimensions; 65 | getDisplayUrl = () => this.#displayUrl; 66 | getTaggedUsers = () => this.#taggedUsers; 67 | isVideo = () => this.#isVideo; 68 | getAccessibilityCaption = () => this.#accessibilityCaption; 69 | areCommentsDisabled = () => this.#commentsDisabled; 70 | getCommentsCount = () => this.#commentsCount; 71 | getLikesCount = () => this.#likesCount; 72 | getTimestamp = () => this.#timestamp; 73 | getLocation = () => this.#location; 74 | getChildren = () => this.#children; 75 | hasAudio = () => this.#hasAudio; 76 | getViewsCount = () => this.#viewsCount; 77 | getVideoUrl = () => this.#videoUrl; 78 | } 79 | 80 | module.exports = Media; 81 | -------------------------------------------------------------------------------- /src/Instagram/Domain/MediaType.js: -------------------------------------------------------------------------------- 1 | class MediaType { 2 | static TYPE_PICTURE = 'picture'; 3 | static TYPE_VIDEO = 'video'; 4 | static TYPE_MULTIPLE_PICTURE = 'multiple_picture'; 5 | 6 | static INSTAGRAM_TYPE_PICTURE = 'GraphImage'; 7 | static INSTAGRAM_TYPE_VIDEO = 'GraphVideo'; 8 | static INASTAGRAM_TYPE_MULTIPLE_PICTURE = 'GraphSidecar'; 9 | 10 | #value; 11 | 12 | constructor(type) { 13 | const matching = {}; 14 | matching[MediaType.INSTAGRAM_TYPE_PICTURE] = MediaType.TYPE_PICTURE; 15 | matching[MediaType.INSTAGRAM_TYPE_VIDEO] = MediaType.TYPE_VIDEO; 16 | matching[MediaType.INASTAGRAM_TYPE_MULTIPLE_PICTURE] = MediaType.TYPE_MULTIPLE_PICTURE; 17 | if (!Object.keys(matching).includes(type)) { 18 | throw new Error(`${type} is not a valid Media type.`); 19 | } 20 | this.#value = matching[type]; 21 | } 22 | 23 | getValue = () => this.#value; 24 | } 25 | 26 | module.exports = MediaType; 27 | -------------------------------------------------------------------------------- /src/Instagram/Domain/MediaVariant.js: -------------------------------------------------------------------------------- 1 | class MediaVariant { 2 | /** @type string */ 3 | #displayUrl; 4 | /** @type Dimensions */ 5 | #dimensions; 6 | 7 | constructor(displayUrl, dimensions) { 8 | this.#displayUrl = displayUrl; 9 | this.#dimensions = dimensions; 10 | } 11 | 12 | getDisplayUrl = () => this.#displayUrl; 13 | getDimensions = () => this.#dimensions; 14 | } 15 | 16 | module.exports = MediaVariant; 17 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Owner.js: -------------------------------------------------------------------------------- 1 | class Owner { 2 | /** @type string */ 3 | #id; 4 | /** @type boolean */ 5 | #isVerified; 6 | /** @type string */ 7 | #profilePicUrl; 8 | /** @type string */ 9 | #username; 10 | /** @type string */ 11 | #fullName; 12 | /** @type boolean */ 13 | #isPrivate; 14 | /** @type number */ 15 | #postsCount; 16 | /** @type number */ 17 | #followersCount; 18 | 19 | constructor( 20 | id, 21 | isVerified, 22 | profilePicUrl, 23 | username, 24 | fullName, 25 | isPrivate, 26 | postsCount, 27 | followersCount 28 | ) { 29 | this.#id = id; 30 | this.#isVerified = isVerified; 31 | this.#profilePicUrl = profilePicUrl; 32 | this.#username = username; 33 | this.#fullName = fullName; 34 | this.#isPrivate = isPrivate; 35 | this.#postsCount = postsCount; 36 | this.#followersCount = followersCount; 37 | } 38 | 39 | getId = () => this.#id; 40 | isVerified = () => this.#isVerified; 41 | getProfilePictureUrl = () => this.#profilePicUrl; 42 | getUsername = () => this.#username; 43 | getFullName = () => this.#fullName; 44 | isPrivate = () => this.#isPrivate; 45 | getPostsCount = () => this.#postsCount; 46 | getFollowersCount = () => this.#followersCount; 47 | } 48 | 49 | module.exports = Owner; 50 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Post.js: -------------------------------------------------------------------------------- 1 | class Post { 2 | /** @type string */ 3 | #id; 4 | /** @type MediaType */ 5 | #type; 6 | /** @type string */ 7 | #shortcode; 8 | /** @type Dimensions */ 9 | #dimensions; 10 | /** @type string */ 11 | #displayUrl; 12 | /** @type string[] */ 13 | #variants; 14 | /** @type string */ 15 | #accessibilityCaption; 16 | /** @type boolean */ 17 | #isVideo; 18 | /** @type TaggedUser[] */ 19 | #taggedUsers; 20 | /** @type string */ 21 | #caption; 22 | /** @type boolean */ 23 | #likeAndViewDisabled; 24 | /** @type number */ 25 | #commentsCount; 26 | /** @type Comment[] */ 27 | #comments; 28 | /** @type boolean */ 29 | #commentsDisabled; 30 | /** @type Date */ 31 | #takenAt; 32 | /** @type number */ 33 | #likesCount; 34 | /** @type boolean */ 35 | #isPaidPartnership; 36 | /** @type Location */ 37 | #location; 38 | /** @type Owner */ 39 | #owner; 40 | /** @type boolean */ 41 | #isAd; 42 | /** @type ChildMedia[] */ 43 | #children; 44 | /** @type boolean */ 45 | #hasAudio; 46 | /** @type number */ 47 | #videoViewsCount; 48 | /** @type number */ 49 | #videoPlaysCount; 50 | 51 | constructor( 52 | id, 53 | type, 54 | shortcode, 55 | dimensions, 56 | displayUrl, 57 | variants, 58 | accessibilityCaption, 59 | isVideo, 60 | taggedUsers, 61 | caption, 62 | likeAndViewDisabled, 63 | commentsCount, 64 | comments, 65 | commentsDisabled, 66 | takenAt, 67 | likesCount, 68 | isPaidPartnership, 69 | location, 70 | owner, 71 | isAd, 72 | children, 73 | hasAudio, 74 | videoViewsCount, 75 | videoPlaysCount 76 | ) { 77 | this.#id = id; 78 | this.#type = type; 79 | this.#shortcode = shortcode; 80 | this.#dimensions = dimensions; 81 | this.#displayUrl = displayUrl; 82 | this.#variants = variants; 83 | this.#accessibilityCaption = accessibilityCaption; 84 | this.#isVideo = isVideo; 85 | this.#taggedUsers = taggedUsers; 86 | this.#caption = caption; 87 | this.#likeAndViewDisabled = likeAndViewDisabled; 88 | this.#commentsCount = commentsCount; 89 | this.#comments = comments; 90 | this.#commentsDisabled = commentsDisabled; 91 | this.#takenAt = takenAt; 92 | this.#likesCount = likesCount; 93 | this.#isPaidPartnership = isPaidPartnership; 94 | this.#location = location; 95 | this.#owner = owner; 96 | this.#isAd = isAd; 97 | this.#children = children; 98 | this.#hasAudio = hasAudio; 99 | this.#videoViewsCount = videoViewsCount; 100 | this.#videoPlaysCount = videoPlaysCount; 101 | } 102 | 103 | getId = () => this.#id; 104 | getType = () => this.#type; 105 | getShortcode = () => this.#shortcode; 106 | getDimensions = () => this.#dimensions; 107 | getDisplayUrl = () => this.#displayUrl; 108 | getVariants = () => this.#variants; 109 | getAccessibilityCaption = () => this.#accessibilityCaption; 110 | isVideo = () => this.#isVideo; 111 | getTaggedUsers = () => this.#taggedUsers; 112 | getCaption = () => this.#caption; 113 | areLikesAndViewsCountDisabled = () => this.#likeAndViewDisabled; 114 | getCommentsCount = () => this.#commentsCount; 115 | getComments = () => this.#comments; 116 | areCommentsDisabled = () => this.#commentsDisabled; 117 | getDate = () => this.#takenAt; 118 | getLikesCount = () => this.#likesCount; 119 | isPaidPartnership = () => this.#isPaidPartnership; 120 | getLocation = () => this.#location; 121 | getOwner = () => this.#owner; 122 | isAd = () => this.#isAd; 123 | getChildren = () => this.#children; 124 | hasAudio = () => this.#hasAudio; 125 | getVideoViewsCount = () => this.#videoViewsCount; 126 | getVideoPlaysCount = () => this.#videoPlaysCount; 127 | } 128 | 129 | module.exports = Post; 130 | -------------------------------------------------------------------------------- /src/Instagram/Domain/Session.js: -------------------------------------------------------------------------------- 1 | class Session { 2 | #id; 3 | #expiryDate; 4 | 5 | /** 6 | * @param {string} id 7 | * @param {Date} expiryDate 8 | */ 9 | constructor(id, expiryDate) { 10 | this.#id = id; 11 | this.#expiryDate = expiryDate; 12 | } 13 | 14 | /** @return string */ 15 | getId = () => this.#id; 16 | /** @returns Date */ 17 | getExpiryDate = () => this.#expiryDate; 18 | } 19 | 20 | module.exports = Session; 21 | -------------------------------------------------------------------------------- /src/Instagram/Domain/TaggedUser.js: -------------------------------------------------------------------------------- 1 | class TaggedUser { 2 | /** @type number */ 3 | #tagXPosition; 4 | /** @type number */ 5 | #tagYPosition; 6 | /** @type string */ 7 | #fullName; 8 | /** @type string */ 9 | #id; 10 | /** @type boolean */ 11 | #isVerified; 12 | /** @type string */ 13 | #profilePictureUrl; 14 | /** @type string */ 15 | #username; 16 | 17 | constructor(tagXPosition, taxYPosition, fullName, id, isVerified, profilePictureUrl, username) { 18 | this.#tagXPosition = tagXPosition; 19 | this.#tagYPosition = taxYPosition; 20 | this.#fullName = fullName; 21 | this.#id = id; 22 | this.#isVerified = isVerified; 23 | this.#profilePictureUrl = profilePictureUrl; 24 | this.#username = username; 25 | } 26 | 27 | getTagXPosition = () => this.#tagXPosition; 28 | getTagYPosition = () => this.#tagYPosition; 29 | getFullName = () => this.#fullName; 30 | getId = () => this.#id; 31 | isVerified = () => this.#isVerified; 32 | getProfilePictureUrl = () => this.#profilePictureUrl; 33 | getUsername = () => this.#username; 34 | } 35 | 36 | module.exports = TaggedUser; 37 | -------------------------------------------------------------------------------- /src/Instagram/Domain/User.js: -------------------------------------------------------------------------------- 1 | class User { 2 | /** @type string */ 3 | #username; 4 | /** @type string */ 5 | #biography; 6 | /** @type number */ 7 | #publicationsCount; 8 | /** @type number */ 9 | #followersCount; 10 | /** @type string */ 11 | #externalUrl; 12 | /** @type number */ 13 | #followingCount; 14 | /** @type string */ 15 | #fullName; 16 | /** @type boolean */ 17 | #hasArEffects; 18 | /** @type boolean */ 19 | #hasClips; 20 | /** @type boolean */ 21 | #hasGuides; 22 | /** @type boolean */ 23 | #hasChannel; 24 | /** @type number */ 25 | #highlightReelCount; 26 | /** @type number */ 27 | #isHidingLikesAndViewsCount; 28 | /** @type string */ 29 | #id; 30 | /** @type boolean */ 31 | #isBusinessAccount; 32 | /** @type boolean */ 33 | #isProfessionalAccount; 34 | /** @type boolean */ 35 | #hasJoinedRecently; 36 | /** @type string */ 37 | #businessAddressJson; 38 | /** @type string */ 39 | #businessContactMethod; 40 | /** @type string */ 41 | #businessEmail; 42 | /** @type string */ 43 | #businessPhoneNumber; 44 | /** @type string */ 45 | #businessCategoryName; 46 | /** @type string */ 47 | #overallCategoryName; 48 | /** @type string */ 49 | #categoryEnum; 50 | /** @type string */ 51 | #categoryName; 52 | /** @type boolean */ 53 | #isPrivate; 54 | /** @type boolean */ 55 | #isVerified; 56 | /** @type string */ 57 | #profilePictureUrl; 58 | /** @type string */ 59 | #profilePictureUrlHd; 60 | /** @type string[] */ 61 | #pronouns; 62 | /** @type Media[] */ 63 | #medias; 64 | 65 | constructor( 66 | username, 67 | biography, 68 | publicationsCount, 69 | followersCount, 70 | followingCount, 71 | externalUrl, 72 | fullName, 73 | hasArEffects, 74 | hasClips, 75 | hasGuides, 76 | hasChannel, 77 | highlightReelsCount, 78 | isHidingLikesAndViewsCount, 79 | id, 80 | isBusinessAccount, 81 | isProfessionalAccount, 82 | hasJoinedRecently, 83 | businessAddressJson, 84 | businessContactMethod, 85 | businessEmail, 86 | businessPhoneNumber, 87 | businessCategoryName, 88 | overallCetagoryName, 89 | categoryEnum, 90 | categoryName, 91 | isPrivate, 92 | isVerified, 93 | profilePictureUrl, 94 | profilePictureUrlHd, 95 | pronouns, 96 | medias, 97 | ) { 98 | this.#username = username; 99 | this.#biography = biography; 100 | this.#publicationsCount = publicationsCount; 101 | this.#followersCount = followersCount; 102 | this.#followingCount = followingCount; 103 | this.#externalUrl = externalUrl; 104 | this.#fullName = fullName; 105 | this.#hasArEffects = hasArEffects; 106 | this.#hasClips = hasClips; 107 | this.#hasGuides = hasGuides; 108 | this.#hasChannel = hasChannel; 109 | this.#highlightReelCount = highlightReelsCount; 110 | this.#isHidingLikesAndViewsCount = isHidingLikesAndViewsCount; 111 | this.#id = id; 112 | this.#isBusinessAccount = isBusinessAccount; 113 | this.#isProfessionalAccount = isProfessionalAccount; 114 | this.#hasJoinedRecently = hasJoinedRecently; 115 | this.#businessAddressJson = businessAddressJson; 116 | this.#businessContactMethod = businessContactMethod; 117 | this.#businessEmail = businessEmail; 118 | this.#businessPhoneNumber = businessPhoneNumber; 119 | this.#businessCategoryName = businessCategoryName; 120 | this.#overallCategoryName = overallCetagoryName; 121 | this.#categoryEnum = categoryEnum; 122 | this.#categoryName = categoryName; 123 | this.#isPrivate = isPrivate; 124 | this.#isVerified = isVerified; 125 | this.#profilePictureUrl = profilePictureUrl; 126 | this.#profilePictureUrlHd = profilePictureUrlHd; 127 | this.#pronouns = pronouns; 128 | this.#medias = medias; 129 | } 130 | 131 | getUsername = () => this.#username; 132 | getBiography = () => this.#biography; 133 | getPublicationsCount = () => this.#publicationsCount; 134 | getFollowersCount = () => this.#followersCount; 135 | getExternalUrl = () => this.#externalUrl; 136 | getFollowingCount = () => this.#followingCount; 137 | getFullName = () => this.#fullName; 138 | hasArEffect = () => this.#hasArEffects; 139 | hasClips = () => this.#hasClips; 140 | hasGuides = () => this.#hasGuides; 141 | hasChannel = () => this.#hasChannel; 142 | getHighlightsReelsCount = () => this.#highlightReelCount; 143 | isHidingLikesAndViewsCount = () => this.#isHidingLikesAndViewsCount; 144 | getId = () => this.#id; 145 | isBusinessAccount = () => this.#isBusinessAccount; 146 | isProfessionalAccount = () => this.#isProfessionalAccount; 147 | hasJoinedRecently = () => this.#hasJoinedRecently; 148 | getBusinessAddressJson = () => this.#businessAddressJson; 149 | getBusinessContactMethod = () => this.#businessContactMethod; 150 | getBusinessEmail = () => this.#businessEmail; 151 | getBusinessPhoneNumber = () => this.#businessPhoneNumber; 152 | getBusinessCategoryName = () => this.#businessCategoryName; 153 | getOverallCategoryName = () => this.#overallCategoryName; 154 | getCategoryEnum = () => this.#categoryEnum; 155 | getCategoryName = () => this.#categoryName; 156 | isPrivate = () => this.#isPrivate; 157 | isVerified = () => this.#isVerified; 158 | getProfilePicture = () => this.#profilePictureUrl; 159 | getHdProfilePicture = () => this.#profilePictureUrlHd; 160 | getPronouns = () => this.#pronouns; 161 | getMedias = () => this.#medias; 162 | } 163 | 164 | module.exports = User; 165 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/InstagramAuthenticationError.js: -------------------------------------------------------------------------------- 1 | class InstagramAuthenticationError extends Error { 2 | } 3 | 4 | module.exports = InstagramAuthenticationError; 5 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/InstagramGenericError.js: -------------------------------------------------------------------------------- 1 | class InstagramGenericError extends Error { 2 | } 3 | 4 | module.exports = InstagramGenericError; 5 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/NoSessionError.js: -------------------------------------------------------------------------------- 1 | class NoSessionError extends Error { 2 | message = 'No session found. Did you authenticate ?'; 3 | } 4 | 5 | module.exports = NoSessionError; 6 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/PostNotFoundError.js: -------------------------------------------------------------------------------- 1 | class PostNotFoundError extends Error { 2 | /** @param {string} shortcode */ 3 | static fromShortcode = (shortcode) => new this(`No post with ${shortcode} as shortcode could be found.`); 4 | } 5 | 6 | module.exports = PostNotFoundError; 7 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/RateLimitError.js: -------------------------------------------------------------------------------- 1 | class RateLimitError extends Error { 2 | constructor() { 3 | super('The server has sent an unsuccessful response. The rate limit has been hit, retry later.'); 4 | } 5 | } 6 | 7 | module.exports = RateLimitError; 8 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/TooManyLoginsError.js: -------------------------------------------------------------------------------- 1 | class TooManyLoginsError extends Error { 2 | } 3 | 4 | module.exports = TooManyLoginsError; 5 | -------------------------------------------------------------------------------- /src/Instagram/Domain/errors/UserNotFoundError.js: -------------------------------------------------------------------------------- 1 | class UserNotFoundError extends Error { 2 | /** @param {string} username */ 3 | static fromUsername = (username) => new this(`No profile with ${username} as username could be found.`); 4 | } 5 | 6 | module.exports = UserNotFoundError; 7 | -------------------------------------------------------------------------------- /src/Instagram/Infrastructure/InstagramRepository.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const axios = require('axios'); 3 | const applicationData = require('../../../app/data.json'); 4 | 5 | const CacheStorage = require('../../../app/services/CacheStorage'); 6 | const CookieReader = require('../../../app/services/CookieReader'); 7 | const InstagramHelper = require('../../../app/services/InstagramHelper'); 8 | const BasicRequestHandler = require('../../../app/services/BasicRequestHandler'); 9 | 10 | const Post = require('../Domain/Post'); 11 | const User = require('../Domain/User'); 12 | 13 | const Media = require('../Domain/Media'); 14 | const Session = require('../Domain/Session'); 15 | const Location = require('../Domain/Location'); 16 | const MediaType = require('../Domain/MediaType'); 17 | const Dimensions = require('../Domain/Dimensions'); 18 | const TaggedUser = require('../Domain/TaggedUser'); 19 | const ChildMedia = require('../Domain/ChildMedia'); 20 | const Comment = require('../Domain/Comment'); 21 | 22 | const RateLimitError = require('../Domain/errors/RateLimitError'); 23 | const NoSessionError = require('../Domain/errors/NoSessionError'); 24 | const UserNotFoundError = require('../Domain/errors/UserNotFoundError'); 25 | const TooManyLoginsError = require('../Domain/errors/TooManyLoginsError'); 26 | const InstagramGenericError = require('../Domain/errors/InstagramGenericError'); 27 | const InstagramAuthenticationError = require('../Domain/errors/InstagramAuthenticationError'); 28 | const PostNotFoundError = require('../Domain/errors/PostNotFoundError'); 29 | const MediaVariant = require('../Domain/MediaVariant'); 30 | const CommentOwner = require('../Domain/CommentOwner'); 31 | const Owner = require('../Domain/Owner'); 32 | 33 | class InstagramRepository { 34 | /** 35 | * @param {string} shortcode 36 | */ 37 | static async getPost(shortcode) { 38 | const queryUrl = applicationData.postUrlTemplate.replace('%SHORTCODE%', shortcode); 39 | 40 | return new Promise((resolve, reject) => { 41 | BasicRequestHandler.handle(queryUrl).then((request) => { 42 | if (typeof request.data === 'string' && request.data.includes('')) { 43 | throw new RateLimitError(); 44 | } 45 | 46 | if (!request.data.hasOwnProperty('graphql')) { 47 | throw PostNotFoundError.fromShortcode(shortcode); 48 | } 49 | 50 | const rawPostData = request.data['graphql']['shortcode_media']; 51 | 52 | const hasComments = rawPostData['edge_media_to_parent_comment']['count'] !== 0; 53 | const hasCaption = rawPostData['edge_media_to_caption']['edges'].length > 0; 54 | const hasTaggedUsers = rawPostData['edge_media_to_tagged_user']['edges'].length > 0; 55 | const hasChildren = rawPostData.hasOwnProperty('edge_sidecar_to_children'); 56 | const isVideo = rawPostData['is_video']; 57 | 58 | resolve(new Post( 59 | rawPostData['id'], 60 | new MediaType(rawPostData['__typename']), 61 | rawPostData['shortcode'], 62 | new Dimensions(rawPostData['height'], rawPostData['width']), 63 | rawPostData['display_url'], 64 | rawPostData['display_resources'].map((resource) => new MediaVariant( 65 | resource['src'], 66 | new Dimensions(resource['config_height'], resource['config_width']) 67 | )), 68 | rawPostData['accessibility_caption'], 69 | isVideo, 70 | hasTaggedUsers 71 | ? rawPostData['edge_media_to_tagged_user']['edges'].map((edge) => { 72 | const node = edge.node; 73 | return new TaggedUser( 74 | node['x'], 75 | node['y'], 76 | node['user']['full_name'], 77 | node['user']['id'], 78 | node['user']['is_verified'], 79 | node['user']['profile_pic_url'], 80 | node['user']['username'] 81 | ); 82 | }) : [], 83 | hasCaption ? rawPostData['edge_media_to_caption']['edges'][0]['node']['text'] : null, 84 | rawPostData['like_and_view_counts_disabled'], 85 | rawPostData['edge_media_to_parent_comment']['count'], 86 | hasComments ? rawPostData['edge_media_to_parent_comment']['edges'].map((objectEdge) => { 87 | const edge = objectEdge.node; 88 | const ownerData = edge['owner']; 89 | return new Comment( 90 | edge['id'], 91 | edge['text'], 92 | new Date(edge['created_at'] * 1000), 93 | new CommentOwner( 94 | ownerData['id'], 95 | ownerData['is_verified'], 96 | ownerData['profile_pic_url'], 97 | ownerData['username'] 98 | ), 99 | edge['edge_liked_by']['count'], 100 | edge['edge_threaded_comments']['count'] !== 0, 101 | edge['edge_threaded_comments']['edges'].map((objectEdge) => { 102 | const edge = objectEdge.node; 103 | return new Comment( 104 | edge['id'], 105 | edge['text'], 106 | new Date(edge['created_at'] * 1000), 107 | new CommentOwner( 108 | ownerData['id'], 109 | ownerData['is_verified'], 110 | ownerData['profile_pic_url'], 111 | ownerData['username'] 112 | ), 113 | edge['edge_liked_by']['count'], 114 | false, 115 | [] 116 | ); 117 | }) 118 | ); 119 | }) : [], 120 | rawPostData['comments_disabled'], 121 | new Date(rawPostData['taken_at_timestamp'] * 1000), 122 | rawPostData['edge_media_preview_like']['count'], 123 | rawPostData['is_paid_partnership'], 124 | rawPostData['location'] 125 | ? new Location( 126 | rawPostData['location']['id'], 127 | rawPostData['location']['has_public_page'], 128 | rawPostData['location']['name'], 129 | rawPostData['location']['slug'] 130 | ) : null, 131 | new Owner( 132 | rawPostData['owner']['id'], 133 | rawPostData['owner']['is_verified'], 134 | rawPostData['owner']['profile_pic_url'], 135 | rawPostData['owner']['username'], 136 | rawPostData['owner']['full_name'], 137 | rawPostData['owner']['is_private'], 138 | rawPostData['owner']['edge_owner_to_timeline_media']['count'], 139 | rawPostData['owner']['edge_followed_by']['count'], 140 | ), 141 | rawPostData['is_ad'], 142 | hasChildren ? rawPostData['edge_sidecar_to_children']['edges'].map((edge) => { 143 | const node = edge.node; 144 | const hasTaggedUsers = node['edge_media_to_tagged_user']['edges'].length > 0; 145 | return new ChildMedia( 146 | new MediaType(node['__typename']), 147 | node['id'], 148 | node['shortcode'], 149 | new Dimensions(node['dimensions']['height'], node['dimensions']['width']), 150 | node['display_url'], 151 | hasTaggedUsers ? node['edge_media_to_tagged_user']['edges'].map((edge) => { 152 | const node = edge.node; 153 | return new TaggedUser( 154 | node['x'], 155 | node['y'], 156 | node['user']['full_name'], 157 | node['user']['id'], 158 | node['user']['is_verified'], 159 | node['user']['profile_pic_url'], 160 | node['user']['username'] 161 | ); 162 | }) : [], 163 | node['accessibility_caption'] 164 | ); 165 | }) : [], 166 | rawPostData['is_video'] ? rawPostData['has_audio'] : false, 167 | rawPostData['is_video'] ? rawPostData['video_view_count'] : null, 168 | rawPostData['is_video'] ? rawPostData['video_play_count'] : null 169 | )); 170 | }).catch((error) => { 171 | if (error instanceof NoSessionError) { 172 | return reject(error); 173 | } 174 | if (error.hasOwnProperty('response') && error.response.status === 404) { 175 | return reject(PostNotFoundError.fromShortcode(shortcode)); 176 | } 177 | return reject(new InstagramGenericError(error.message)); 178 | }); 179 | }); 180 | } 181 | 182 | /** 183 | * @param {string} username 184 | * @returns Promise 185 | */ 186 | static async getUser(username) { 187 | const queryUrl = applicationData.usernameUrlTemplate.replace('%USERNAME%', username); 188 | 189 | return new Promise((resolve, reject) => { 190 | BasicRequestHandler.handle(queryUrl).then((request) => { 191 | if (typeof request.data === 'string' && request.data.includes('')) { 192 | throw new RateLimitError(); 193 | } 194 | 195 | if (!request.data.hasOwnProperty('graphql')) { 196 | throw UserNotFoundError.fromUsername(username); 197 | } 198 | 199 | const rawUserData = request.data['graphql']['user']; 200 | 201 | const hasPosts = rawUserData['edge_owner_to_timeline_media']['edges'].length > 0; 202 | 203 | resolve(new User( 204 | rawUserData.username, 205 | rawUserData['biography'], 206 | rawUserData['edge_owner_to_timeline_media']['count'], 207 | rawUserData['edge_followed_by']['count'], 208 | rawUserData['edge_follow']['count'], 209 | rawUserData['external_url'], 210 | rawUserData['full_name'], 211 | rawUserData['has_ar_effects'], 212 | rawUserData['has_clips'], 213 | rawUserData['has_guides'], 214 | rawUserData['has_channel'], 215 | rawUserData['highlight_reel_count'], 216 | rawUserData['hide_like_and_view_counts'], 217 | rawUserData['id'], 218 | rawUserData['is_business_account'], 219 | rawUserData['is_professional_account'], 220 | rawUserData['is_joined_recently'], 221 | rawUserData['business_address_json'], 222 | rawUserData['business_contact_method'], 223 | rawUserData['business_email'], 224 | rawUserData['business_phone_number'], 225 | rawUserData['business_category_name'], 226 | rawUserData['overall_category_name'], 227 | rawUserData['category_enum'], 228 | rawUserData['category_name'], 229 | rawUserData['is_private'], 230 | rawUserData['is_verified'], 231 | rawUserData['profile_pic_url'], 232 | rawUserData['profile_pic_url_hd'], 233 | rawUserData['pronouns'], 234 | hasPosts ? rawUserData['edge_owner_to_timeline_media']['edges'].map((edge) => { 235 | const node = edge.node; 236 | const hasChildren = node.hasOwnProperty('edge_sidecar_to_children'); 237 | const hasTaggedUsers = node['edge_media_to_tagged_user']['edges'].length > 0; 238 | const hasCaption = node['edge_media_to_caption']['edges'].length > 0; 239 | return new Media( 240 | new MediaType(node['__typename']), 241 | node['id'], 242 | node['shortcode'], 243 | hasCaption ? node['edge_media_to_caption']['edges'][0]['node']['text'] : null, 244 | new Dimensions(node['dimensions']['height'], node['dimensions']['width']), 245 | node['display_url'], 246 | hasTaggedUsers 247 | ? node['edge_media_to_tagged_user']['edges'].map((edge) => { 248 | const node = edge.node; 249 | return new TaggedUser( 250 | node['x'], 251 | node['y'], 252 | node['user']['full_name'], 253 | node['user']['id'], 254 | node['user']['is_verified'], 255 | node['user']['profile_pic_url'], 256 | node['user']['username'] 257 | ); 258 | }) : [], 259 | node['is_video'], 260 | node['accessibility_caption'], 261 | node['comments_disabled'], 262 | node['edge_media_to_comment']['count'], 263 | node['edge_liked_by']['count'], 264 | node['taken_at_timestamp'], 265 | node['location'] ? new Location(node['location']['id'], node['location']['has_public_page'], node['location']['name'], node['location']['slug']) : null, 266 | hasChildren 267 | ? node['edge_sidecar_to_children']['edges'].map((edge) => { 268 | const node = edge.node; 269 | const hasTaggedUsers = node['edge_media_to_tagged_user']['edges'].length > 0; 270 | return new ChildMedia( 271 | new MediaType(node['__typename']), 272 | node['id'], 273 | node['shortcode'], 274 | new Dimensions(node['dimensions']['height'], node['dimensions']['width']), 275 | node['display_url'], 276 | hasTaggedUsers 277 | ? node['edge_media_to_tagged_user']['edges'].map((edge) => { 278 | const node = edge.node; 279 | return new TaggedUser( 280 | node['x'], 281 | node['y'], 282 | node['user']['full_name'], 283 | node['user']['id'], 284 | node['user']['is_verified'], 285 | node['user']['profile_pic_url'], 286 | node['user']['username'] 287 | ); 288 | }) : [], 289 | node['accessibility_caption'] 290 | ); 291 | }) : [], 292 | node['has_audio'], 293 | node['video_view_count'], 294 | node['video_url'] 295 | ); 296 | }) : [] 297 | )); 298 | }).catch((error) => { 299 | if (error instanceof NoSessionError) { 300 | return reject(error); 301 | } 302 | if (error.hasOwnProperty('response') && error.response.status === 404) { 303 | return reject(UserNotFoundError.fromUsername(username)); 304 | } 305 | return reject(new InstagramGenericError(error.message)); 306 | }); 307 | }); 308 | } 309 | 310 | static async authenticate() { 311 | const request = await axios.get(InstagramHelper.BASE_URL, { 312 | headers: { 313 | 'user-agent': InstagramHelper.DEFAULT_USER_AGENT 314 | } 315 | }); 316 | 317 | const matches = request.data.match(/