├── .rspec ├── lib ├── ig_api │ ├── version.rb │ ├── configuration.rb │ ├── constants.rb │ ├── relationship.rb │ ├── media.rb │ ├── feed.rb │ ├── thread.rb │ ├── user.rb │ ├── http.rb │ └── account.rb └── ig_api.rb ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Rakefile ├── bin ├── setup └── console ├── Gemfile ├── README.md ├── spec ├── instagram_spec.rb └── spec_helper.rb ├── Gemfile.lock ├── LICENSE.txt ├── ig_api.gemspec └── CODE_OF_CONDUCT.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/ig_api/version.rb: -------------------------------------------------------------------------------- 1 | module IgApi 2 | VERSION = "0.0.25" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | Gemfile.lock 3 | *.gem 4 | Gemfile.lock 5 | .vscode 6 | .ruby-version 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | Layout/EndOfLine: 4 | EnforcedStyle: lf -------------------------------------------------------------------------------- /lib/ig_api/configuration.rb: -------------------------------------------------------------------------------- 1 | module IgApi 2 | class Configuration 3 | attr_accessor :proxy_list 4 | end 5 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.2 5 | before_install: gem install bundler -v 1.16.0 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in ig_api-Api.gemspec 6 | gemspec -------------------------------------------------------------------------------- /lib/ig_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ig_api/http' 4 | require 'ig_api/user' 5 | require 'ig_api/media' 6 | require 'ig_api/device' 7 | require 'ig_api/thread' 8 | require 'ig_api/constants' 9 | require 'ig_api/relationship' 10 | require 'ig_api/media' 11 | 12 | # Root module 13 | module IgApi 14 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instagram::API 2 | 3 | Welcome to Instagram API gem! originally implemented from [huttarichard/instagram-private-api](https://github.com/huttarichard/instagram-private-api) 4 | 5 | ## Installation 6 | 7 | This repo is no longer maintained. 8 | 9 | Visit [ryanckulp/instagram-private-api](https://github.com/ryanckulp/instagram-private-api) to contribute to the latest version. 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'ig_api' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/instagram_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'ig_api' 3 | 4 | describe 'ig_api' do 5 | it 'should login' do 6 | # p IgApi::Media.get_id_from_code 'BlvwDHwFSgy' 7 | account = IgApi::Account.new 8 | @user = account.using ENV['INSTAGRAM_SESSION'] 9 | 10 | expect(@user).to be_instance_of IgApi::User 11 | 12 | @search = @user.info_by_name'vicoerv' 13 | @user_id = @search.user.pk 14 | 15 | media = IgApi::Media.new(@user) 16 | likes = media.like('1735389257830282125_1626347005') 17 | 18 | @user.timeline_media 19 | end 20 | end -------------------------------------------------------------------------------- /lib/ig_api/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module IgApi 4 | module Constants 5 | PRIVATE_KEY = { 6 | SIG_KEY: '673581b0ddb792bf47da5f9ca816b613d7996f342723aa06993a3f0552311c7d', 7 | SIG_VERSION: '4', 8 | APP_VERSION: '42.0.0.19.95' 9 | }.freeze 10 | 11 | HEADER = { 12 | capabilities: '3brTPw==', 13 | type: 'WIFI', 14 | host: 'i.instagram.com', 15 | connection: 'Close', 16 | encoding: 'gzip, deflate, sdch', 17 | accept: '*/*' 18 | }.freeze 19 | 20 | URL = 'https://i.instagram.com/api/v1/' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ig_api (0.0.25) 5 | multipart-post (~> 2.0.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | diff-lcs (1.3) 11 | multipart-post (2.0.0) 12 | rake (10.5.0) 13 | rspec (3.8.0) 14 | rspec-core (~> 3.8.0) 15 | rspec-expectations (~> 3.8.0) 16 | rspec-mocks (~> 3.8.0) 17 | rspec-core (3.8.0) 18 | rspec-support (~> 3.8.0) 19 | rspec-expectations (3.8.2) 20 | diff-lcs (>= 1.2.0, < 2.0) 21 | rspec-support (~> 3.8.0) 22 | rspec-mocks (3.8.0) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.8.0) 25 | rspec-support (3.8.0) 26 | 27 | PLATFORMS 28 | ruby 29 | x64-mingw32 30 | 31 | DEPENDENCIES 32 | bundler (~> 1.16) 33 | ig_api! 34 | rake (~> 10.0) 35 | rspec (~> 3.0) 36 | 37 | BUNDLED WITH 38 | 1.17.1 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 atsuko_maeda_official 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/ig_api/relationship.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'ostruct' 3 | 4 | module IgApi 5 | class Relationship 6 | def initialize user 7 | @user = user 8 | @api = nil 9 | end 10 | 11 | def create(id) 12 | JSON.parse api.post("https://i.instagram.com/api/v1/friendships/create/#{id}/", 13 | format( 14 | 'ig_sig_key_version=4&signed_body=%s', 15 | Http.generate_signature( 16 | user_id: id 17 | ) 18 | )).with(session: @user.session, ua: @user.useragent) 19 | .exec.body, object_class: OpenStruct 20 | end 21 | 22 | def destroy(id) 23 | JSON.parse api.post("https://i.instagram.com/api/v1/friendships/destroy/#{id}/", 24 | format( 25 | 'ig_sig_key_version=4&signed_body=%s', 26 | Http.generate_signature( 27 | user_id: id 28 | ) 29 | )).with(session: @user.session, ua: @user.useragent) 30 | .exec.body, object_class: OpenStruct 31 | end 32 | 33 | def api 34 | @api = Http.new if @api.nil? 35 | 36 | @api 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /ig_api.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ig_api/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'ig_api' 8 | spec.version = IgApi::VERSION 9 | spec.authors = ['vicoerv', 'ryanckulp'] 10 | spec.email = ['vicoerv@gmail.com'] 11 | 12 | spec.summary = 'Instagram private api' 13 | spec.description = 'implemented from huttarichard/instagram-private-api' 14 | spec.homepage = 'http://www.vicoervanda.com' 15 | spec.license = 'MIT' 16 | 17 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 18 | # to allow pushing to a single host or delete this section to allow pushing to any host. 19 | if spec.respond_to?(:metadata) 20 | spec.metadata['allowed_push_host'] = "https://rubygems.org" 21 | else 22 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 23 | 'public gem pushes.' 24 | end 25 | 26 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 27 | f.match(%r{^(test|spec|features)/}) 28 | end 29 | spec.bindir = 'exe' 30 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 31 | spec.require_paths = ['lib'] 32 | 33 | spec.add_development_dependency 'bundler', '~> 1.16' 34 | spec.add_development_dependency 'rake', '~> 10.0' 35 | spec.add_development_dependency 'rspec', '~> 3.0' 36 | 37 | spec.add_dependency 'multipart-post', '~> 2.0.0' 38 | end 39 | -------------------------------------------------------------------------------- /lib/ig_api/media.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module IgApi 4 | class Media 5 | def self.get_id_from_code(code) 6 | alphabet = { 7 | '-': 62, '1': 53, '0': 52, '3': 55, '2': 54, '5': 57, 8 | '4': 56, '7': 59, '6': 58, '9': 91, '8': 60, 'A': 0, 9 | 'C': 2, 'B': 1, 'E': 4, 'D': 3, 'G': 6, 'F': 5, 'I': 8, 10 | 'H': 7, 'K': 10, 'J': 9, 'M': 12, 'L': 11, 'O': 14, 'N': 13, 11 | 'Q': 16, 'P': 15, 'S': 18, 'R': 17, 'U': 20, 'T': 19, 'W': 22, 12 | 'V': 21, 'Y': 24, 'X': 23, 'Z': 25, '_': 63, 'a': 26, 'c': 28, 13 | 'b': 27, 'e': 30, 'd': 29, 'g': 32, 'f': 31, 'i': 34, 'h': 33, 14 | 'k': 36, 'j': 35, 'm': 38, 'l': 37, 'o': 40, 'n': 39, 'q': 42, 15 | 'p': 41, 's': 44, 'r': 43, 'u': 46, 't': 45, 'w': 48, 'v': 47, 16 | 'y': 50, 'x': 49, 'z': 51 17 | } 18 | 19 | n = 0 20 | 21 | code.split(//).each do |c| 22 | n = n * 64 + alphabet[:"#{c}"] 23 | end 24 | 25 | n 26 | end 27 | 28 | def initialize(user) 29 | @user = user 30 | @api = Http.singleton 31 | end 32 | 33 | def create_like(media_id) 34 | response = @api.post(Constants::URL + "media/#{media_id}/like/") 35 | .with(ua: @user.useragent, session: @user.session) 36 | .exec 37 | 38 | JSON.parse response.body, object_class: OpenStruct 39 | end 40 | 41 | def like(media_id) 42 | response = @api.get(Constants::URL + "media/#{media_id}/likers/") 43 | .with(ua: @user.useragent, session: @user.session) 44 | .exec 45 | 46 | raise Exception, response['message'] if response['status'] == 'fail' 47 | 48 | JSON.parse response.body, object_class: OpenStruct 49 | end 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /lib/ig_api/feed.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module IgApi 4 | class Feed 5 | def initialize 6 | @api = Http.singleton 7 | end 8 | 9 | def using user 10 | @user = { 11 | id: user.data[:pk], 12 | session: user.session, 13 | ua: user.useragent 14 | } 15 | self 16 | end 17 | 18 | def story(ids) 19 | signature = IgApi::Http.generate_signature( 20 | user_ids: ids.map(&:to_s) 21 | ) 22 | response = @api.post(Constants::URL + 'feed/reels_media/', 23 | "ig_sig_key_version=4&signed_body=#{signature}") 24 | .with(session: @user[:session], ua: @user[:ua]) 25 | .exec 26 | 27 | response.body 28 | end 29 | 30 | def timeline_media(params = {}) 31 | user_id = @user[:id] 32 | 33 | rank_token = IgApi::Http.generate_rank_token @user[:id] 34 | endpoint = Constants::URL + "feed/user/#{user_id}/" 35 | endpoint << "?rank_token=#{rank_token}" 36 | params.each { |k, v| endpoint << "&#{k}=#{v}" } 37 | result = @api.get(endpoint) 38 | .with(session: @user[:session], ua: @user[:ua]) 39 | .exec 40 | 41 | JSON.parse result.body, object_class: OpenStruct 42 | end 43 | 44 | def self.user_followers(user, data, limit) 45 | has_next_page = true 46 | followers = [] 47 | user_id = (!data[:id].nil? ? data[:id] : user.data[:id]) 48 | data[:rank_token] = IgApi::API.generate_rank_token user.session.scan(/ds_user_id=([\d]+);/)[0][0] 49 | while has_next_page && limit > followers.size 50 | response = user_followers_next_page(user, user_id, data) 51 | has_next_page = !response['next_max_id'].nil? 52 | data[:max_id] = response['next_max_id'] 53 | followers += response['users'] 54 | end 55 | limit.infinite? ? followers : followers[0...limit] 56 | end 57 | 58 | def self.user_followers_next_page(user, user_id, data) 59 | endpoint = "https://i.instagram.com/api/v1/friendships/#{user_id}/followers/" 60 | param = "?rank_token=#{data[:rank_token]}" + 61 | (!data[:max_id].nil? ? '&max_id=' + data[:max_id] : '') 62 | result = IgApi::API.http( 63 | url: endpoint + param, 64 | method: 'GET', 65 | user: user 66 | ) 67 | JSON.parse result.body, object_class: OpenStruct 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/ig_api/thread.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'net/http/post/multipart' 3 | 4 | module IgApi 5 | class Thread 6 | def initialize 7 | @api = Http.singleton 8 | end 9 | 10 | def using(user) 11 | @user = { 12 | id: user.data[:id], 13 | session: user.session, 14 | ua: user.useragent 15 | } 16 | 17 | self 18 | end 19 | 20 | def configure_text(users, text) 21 | uris = URI.extract(text, %w[http https]) 22 | broadcast = 'text' 23 | 24 | body = { 25 | recipient_users: [users].to_json, 26 | client_context: Http.generate_uuid, 27 | } 28 | 29 | if uris.empty? 30 | body[:text] = text 31 | else 32 | broadcast = 'link' 33 | body[:link_text] = text 34 | body[:link_urls] = uris.to_json 35 | end 36 | 37 | response = @api.multipart(Constants::URL + 38 | "direct_v2/threads/broadcast/#{broadcast}/", 39 | body) 40 | .with(ua: @user[:ua], session: @user[:session]) 41 | .exec 42 | 43 | response.body 44 | end 45 | 46 | def configure_media(users, media_id, text) 47 | payload = { 48 | recipient_users: [users].to_json, 49 | client_context: IgApi::Http.generate_uuid, 50 | media_id: media_id 51 | } 52 | 53 | payload[:text] = text unless text.empty? 54 | response = @api.multipart(Constants::URL + 'direct_v2/threads/broadcast/media_share/?media_type=photo', 55 | payload) 56 | .with(session: @user[:session], ua: @user[:ua]) 57 | .exec 58 | 59 | response.body 60 | end 61 | 62 | def configure_story(users, media_id, text) 63 | payload = { 64 | action: 'send_item', 65 | _uuid: IgApi::Http.generate_uuid, 66 | client_context: IgApi::Http.generate_uuid, 67 | recipient_users: [users].to_json, 68 | story_media_id: media_id, 69 | reel_id: media_id.split('_')[1], 70 | text: text 71 | } 72 | 73 | signature = Http.generate_signature payload 74 | 75 | response = @api.post( 76 | Constants::URL + 'direct_v2/threads/broadcast/story_share/', 77 | "ig_sig_key_version=4&signed_body=#{signature}" 78 | ) 79 | .with(ua: @user[:ua], session: @user[:session]) 80 | .exec 81 | 82 | response.body 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at vicoerv@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/ig_api/user.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'ig_api/device' 3 | require 'ig_api/constants' 4 | 5 | module IgApi 6 | class User 7 | attr_reader :password, :language 8 | attr_accessor :username, :config, :session, :data 9 | 10 | def initialize(params = {}) 11 | @account = nil 12 | @feed = nil 13 | @api = IgApi::Http.singleton 14 | 15 | if params.key? :session 16 | @username = params[:session].scan(/ds_user=(.*?);/)[0][0] 17 | 18 | id = params[:session].scan(/ds_user_id=(\d+)/)[0][0] 19 | 20 | if data.nil? 21 | @data = { id: id } 22 | else 23 | @data[:id] = id 24 | end 25 | end 26 | 27 | inject_variables(params) 28 | end 29 | 30 | def inject_variables(params) 31 | params.each { |key, value| instance_variable_set(:"@#{key}", value) } 32 | end 33 | 34 | def search_for_user(username) 35 | account.search_for_user(self, username) 36 | end 37 | 38 | def direct_messages(limit = nil) 39 | account.list_direct_messages(self, limit) 40 | end 41 | 42 | def info_by_name(username) 43 | response = @api.get(Constants::URL + "users/#{username}/usernameinfo/") 44 | .with(ua: useragent, session: session) 45 | .exec 46 | 47 | JSON.parse response.body, object_class: OpenStruct 48 | end 49 | 50 | def search_for_user_graphql(username) 51 | account.search_for_graphql(self, username) 52 | end 53 | 54 | def followers(limit = Float::INFINITY, data = {}) 55 | IgApi::Feed.user_followers(self, data, limit) 56 | end 57 | 58 | def user_followers_graphql(limit = Float::INFINITY, data = {}) 59 | IgApi::Feed.user_followers_graphql(self, data, limit) 60 | end 61 | 62 | def relationship 63 | unless instance_variable_defined? :@relationship 64 | @relationship = Relationship.new self 65 | end 66 | 67 | @relationship 68 | end 69 | 70 | def account 71 | @account = IgApi::Account.new if @account.nil? 72 | 73 | @account 74 | end 75 | 76 | def feed 77 | @feed = IgApi::Feed.new if @feed.nil? 78 | 79 | @feed.using(self) 80 | end 81 | 82 | def thread 83 | @thread = IgApi::Thread.new unless defined? @thread 84 | 85 | @thread.using self 86 | end 87 | 88 | def md5 89 | Digest::MD5.hexdigest @username 90 | end 91 | 92 | def md5int 93 | (md5.to_i(32) / 10e32).round 94 | end 95 | 96 | def api 97 | (18 + (md5int % 5)).to_s 98 | end 99 | 100 | # @return [string] 101 | def release 102 | %w[4.0.4 4.3.1 4.4.4 5.1.1 6.0.1][md5int % 5] 103 | end 104 | 105 | def dpi 106 | %w[801 577 576 538 515 424 401 373][md5int % 8] 107 | end 108 | 109 | def resolution 110 | %w[3840x2160 1440x2560 2560x1440 1440x2560 111 | 2560x1440 1080x1920 1080x1920 1080x1920][md5int % 8] 112 | end 113 | 114 | def info 115 | line = Device.devices[md5int % Device.devices.count] 116 | { 117 | manufacturer: line[0], 118 | device: line[1], 119 | model: line[2] 120 | } 121 | end 122 | 123 | def useragent_hash 124 | agent = [api + '/' + release, dpi + 'dpi', 125 | resolution, info[:manufacturer], 126 | info[:model], info[:device], @language] 127 | 128 | { 129 | agent: agent.join('; '), 130 | version: Constants::PRIVATE_KEY[:APP_VERSION] 131 | } 132 | end 133 | 134 | def useragent 135 | format('Instagram %s Android(%s)', useragent_hash[:version], useragent_hash[:agent].rstrip) 136 | end 137 | 138 | def device_id 139 | 'android-' + md5[0..15] 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/ig_api/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ig_api/version' 4 | require 'openssl' 5 | require 'net/http' 6 | require 'json' 7 | require 'ig_api/user' 8 | require 'ig_api/account' 9 | require 'ig_api/feed' 10 | require 'ig_api/configuration' 11 | 12 | module IgApi 13 | class Http 14 | def self.compute_hash(data) 15 | OpenSSL::HMAC.hexdigest OpenSSL::Digest.new('sha256'), Constants::PRIVATE_KEY[:SIG_KEY], data 16 | end 17 | 18 | def self.__obj=(value) 19 | @@obj = value 20 | end 21 | 22 | def self.__obj 23 | @@obj 24 | end 25 | 26 | def self.singleton 27 | @@obj = Http.new unless defined? @@obj 28 | 29 | @@obj 30 | end 31 | 32 | def self.generate_uuid 33 | 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.gsub(/[xy]/) do |c| 34 | r = (Random.rand * 16).round | 0 35 | v = c == 'x' ? r : (r & 0x3 | 0x8) 36 | c.gsub(c, v.to_s(16)) 37 | end.downcase 38 | end 39 | 40 | def self.create_md5(data) 41 | Digest::MD5.hexdigest(data).to_s 42 | end 43 | 44 | def self.generate_device_id 45 | timestamp = Time.now.to_i.to_s 46 | 'android-' + create_md5(timestamp)[0..16] 47 | end 48 | 49 | def self.generate_signature(data) 50 | data = data.to_json 51 | hash = compute_hash(data) + '.' + data 52 | CGI.escape(hash) 53 | end 54 | 55 | def post(url, body = nil) 56 | @data = { method: 'POST', url: url, body: body } 57 | self 58 | end 59 | 60 | def multipart(url, body = nil) 61 | @data = { method: 'MULTIPART', url: url, body: body } 62 | self 63 | end 64 | 65 | def with(data) 66 | data.each { |k, v| @data[k] = v } 67 | self 68 | end 69 | 70 | def exec 71 | http @data 72 | end 73 | 74 | def get(url) 75 | @data = {method: 'GET', url: url} 76 | self 77 | end 78 | 79 | def http(args) 80 | args[:url] = URI.parse(args[:url]) 81 | http = Net::HTTP.new(args[:url].host, args[:url].port, 82 | ENV['INSTAGRAM_PROXY_HOST'], ENV['INSTAGRAM_PROXY_PORT']) 83 | http.use_ssl = true 84 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 85 | request = nil 86 | if args[:method] == 'POST' 87 | request = Net::HTTP::Post.new(args[:url].path) 88 | elsif args[:method] == 'GET' 89 | request = Net::HTTP::Get.new(args[:url].path + (!args[:url].query.nil? ? '?' + args[:url].query : '')) 90 | elsif args[:method] == 'MULTIPART' 91 | request = Net::HTTP::Post::Multipart.new args[:url].path, args[:body], 92 | 'User-Agent': args[:ua], 93 | Accept: IgApi::Constants::HEADER[:accept], 94 | 'Accept-Encoding': IgApi::Constants::HEADER[:encoding], 95 | 'Accept-Language': 'en-US', 96 | 'X-IG-Capabilities': IgApi::Constants::HEADER[:capabilities], 97 | 'X-IG-Connection-Type': IgApi::Constants::HEADER[:type], 98 | Cookie: args[:session] || '' 99 | end 100 | 101 | unless args[:method] == 'MULTIPART' 102 | request.initialize_http_header('User-Agent': args[:ua], 103 | Accept: IgApi::Constants::HEADER[:accept], 104 | 'Accept-Encoding': IgApi::Constants::HEADER[:encoding], 105 | 'Accept-Language': 'en-US', 106 | 'X-IG-Capabilities': IgApi::Constants::HEADER[:capabilities], 107 | 'X-IG-Connection-Type': IgApi::Constants::HEADER[:type], 108 | Cookie: args[:session] || '') 109 | 110 | request.body = args.key?(:body) ? args[:body] : nil 111 | end 112 | 113 | http.request(request) 114 | end 115 | 116 | def self.generate_rank_token(pk) 117 | format('%s_%s', pk, IgApi::Http.generate_uuid) 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # This setting enables warnings. It's recommended, but in some cases may 70 | # be too noisy due to issues in dependencies. 71 | config.warnings = true 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /lib/ig_api/account.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module IgApi 4 | class Account 5 | def initialized 6 | @api = nil 7 | end 8 | 9 | def api 10 | @api = IgApi::Http.new if @api.nil? 11 | 12 | @api 13 | end 14 | 15 | def using(session) 16 | User.new session: session 17 | end 18 | 19 | def login(username, password, config = IgApi::Configuration.new) 20 | user = User.new username: username, 21 | password: password 22 | 23 | request = api.post( 24 | Constants::URL + 'accounts/login/', 25 | format( 26 | 'ig_sig_key_version=4&signed_body=%s', 27 | IgApi::Http.generate_signature( 28 | device_id: user.device_id, 29 | login_attempt_user: 0, password: user.password, username: user.username, 30 | _csrftoken: 'missing', _uuid: IgApi::Http.generate_uuid 31 | ) 32 | ) 33 | ).with(ua: user.useragent).exec 34 | 35 | 36 | response = JSON.parse request.body, object_class: OpenStruct 37 | 38 | return response if response.error_type == 'checkpoint_challenge_required' 39 | 40 | raise response.message if response.status == 'fail' 41 | 42 | logged_in_user = response.logged_in_user 43 | user.data = logged_in_user 44 | 45 | cookies_array = [] 46 | all_cookies = request.get_fields('set-cookie') || [] 47 | all_cookies.each do |cookie| 48 | cookies_array.push(cookie.split('; ')[0]) 49 | end 50 | cookies = cookies_array.join('; ') 51 | user.config = config 52 | user.session = cookies 53 | 54 | user 55 | end 56 | 57 | def self.search_for_user_graphql(user, username) 58 | endpoint = "https://www.instagram.com/#{username}/?__a=1" 59 | result = IgApi::API.http(url: endpoint, method: 'GET', user: user) 60 | 61 | response = JSON.parse result.body, symbolize_names: true, object_class: OpenStruct 62 | return nil unless response.user.any? 63 | end 64 | 65 | def search_for_user(user, username) 66 | rank_token = IgApi::Http.generate_rank_token user.session.scan(/ds_user_id=([\d]+);/)[0][0] 67 | endpoint = 'https://i.instagram.com/api/v1/users/search/' 68 | param = format('?is_typehead=true&q=%s&rank_token=%s', username, rank_token) 69 | result = api.get(endpoint + param) 70 | .with(session: user.session, ua: user.useragent).exec 71 | 72 | result = JSON.parse result.body, object_class: OpenStruct 73 | 74 | if result.num_results > 0 75 | user_result = result.users[0] 76 | user_object = IgApi::User.new username: username 77 | user_object.data = user_result 78 | user_object.session = user.session 79 | user_object 80 | end 81 | end 82 | 83 | def list_direct_messages(user, limit = 100) 84 | base_url = 'https://i.instagram.com/api/v1' 85 | rank_token = IgApi::Http.generate_rank_token user.session.scan(/ds_user_id=([\d]+);/)[0][0] 86 | 87 | inbox_params = "?persistentBadging=true&use_unified_inbox=true&show_threads=true&limit=#{limit}" 88 | 89 | # each type of message requires a uniqe fetch 90 | inbox_endpoint = base_url + "/direct_v2/inbox/#{inbox_params}" 91 | inbox_pending_endpoint = base_url + "/direct_v2/pending_inbox/#{inbox_params}" 92 | 93 | param = format('&is_typehead=true&q=%s&rank_token=%s', user.username, rank_token) 94 | 95 | inbox_result = api.get(inbox_endpoint + param).with(session: user.session, ua: user.useragent).exec 96 | inbox_result = JSON.parse inbox_result.body, object_class: OpenStruct 97 | 98 | inbox_pending_result = api.get(inbox_pending_endpoint + param).with(session: user.session, ua: user.useragent).exec 99 | inbox_pending_result = JSON.parse inbox_pending_result.body, object_class: OpenStruct 100 | 101 | threads = ((inbox_result.inbox.threads || []) + (inbox_pending_result.inbox.threads || [])).flatten 102 | all_messages = [] 103 | 104 | # fetch + combine past messages from parent thread 105 | threads.each do |thread| 106 | # thread_id = thread.thread_v2_id # => 17953972372244048 DO NOT USE V2! 107 | thread_id = thread.thread_id # => 340282366841710300949128223810596505168 108 | cursor_id = thread.oldest_cursor # '28623389310319272791051433794338816' 109 | 110 | thread_endpoint = base_url + "/direct_v2/threads/#{thread_id}/?cursor=#{cursor_id}" 111 | param = format('&is_typehead=true&q=%s&rank_token=%s', user.username, rank_token) 112 | 113 | result = api.get(thread_endpoint + param).with(session: user.session, ua: user.useragent).exec 114 | result = JSON.parse result.body, object_class: OpenStruct 115 | 116 | if result.thread && result.thread.items.count > 0 117 | older_messages = result.thread.items.sort_by(&:timestamp) # returns oldest --> newest 118 | 119 | all_messages << { 120 | thread_id: thread_id, 121 | recipient_username: thread.users.first.try(:username), # possible to have 1+ or none (e.g. 'mention') 122 | conversations: older_messages << thread.items.first 123 | } 124 | elsif result.thread && result.thread.last_permanent_item 125 | all_messages << { 126 | thread_id: thread_id, 127 | recipient_username: thread.users.first.try(:username), 128 | conversations: result.thread.last_permanent_item 129 | } 130 | end 131 | end 132 | 133 | all_messages 134 | end 135 | end 136 | end 137 | --------------------------------------------------------------------------------