├── .rspec ├── Gemfile ├── spec ├── fixtures │ ├── media_liked.json │ ├── media_unliked.json │ ├── media_comment_deleted.json │ ├── subscription_deleted.json │ ├── tag.json │ ├── user_search.json │ ├── block_user.json │ ├── deny_user.json │ ├── unblock_user.json │ ├── approve_user.json │ ├── follow_user.json │ ├── unfollow_user.json │ ├── location_search_fsq.json │ ├── location.json │ ├── mikeyk.json │ ├── shayne.json │ ├── media_comment.json │ ├── relationship.json │ ├── access_token.json │ ├── subscription.json │ ├── requested_by.json │ ├── subscription_payload.json │ ├── oembed.json │ ├── media_likes.json │ ├── media_comments.json │ ├── subscriptions.json │ ├── media.json │ ├── location_search.json │ ├── media_shortcode.json │ ├── tag_search.json │ ├── follows.json │ ├── followed_by.json │ └── media_search.json ├── instagram │ ├── client_spec.rb │ ├── client │ │ ├── utils_spec.rb │ │ ├── embedding_spec.rb │ │ ├── geography_spec.rb │ │ ├── likes_spec.rb │ │ ├── comments_spec.rb │ │ ├── tags_spec.rb │ │ ├── media_spec.rb │ │ ├── locations_spec.rb │ │ ├── subscriptions_spec.rb │ │ └── users_spec.rb │ ├── request_spec.rb │ └── api_spec.rb ├── spec_helper.rb ├── faraday │ └── response_spec.rb └── instagram_spec.rb ├── .travis.yml ├── lib ├── instagram │ ├── version.rb │ ├── response.rb │ ├── client.rb │ ├── api.rb │ ├── client │ │ ├── utils.rb │ │ ├── embedding.rb │ │ ├── geographies.rb │ │ ├── likes.rb │ │ ├── tags.rb │ │ ├── comments.rb │ │ ├── locations.rb │ │ ├── media.rb │ │ ├── subscriptions.rb │ │ └── users.rb │ ├── connection.rb │ ├── error.rb │ ├── oauth.rb │ ├── request.rb │ └── configuration.rb ├── instagram.rb └── faraday │ ├── oauth2.rb │ └── raise_http_exception.rb ├── .gitignore ├── .yardopts ├── Rakefile ├── PATENTS.md ├── LICENSE.md ├── instagram.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | --backtrace -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/fixtures/media_liked.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": null} -------------------------------------------------------------------------------- /spec/fixtures/media_unliked.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": null} -------------------------------------------------------------------------------- /spec/fixtures/media_comment_deleted.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": null} -------------------------------------------------------------------------------- /spec/fixtures/subscription_deleted.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": null} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.2 4 | - 2.0.0 5 | - 1.9.3 6 | -------------------------------------------------------------------------------- /lib/instagram/version.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | VERSION = '1.1.3'.freeze unless defined?(::Instagram::VERSION) 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/tag.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"description": null, "media_count": 6697, "name": "cat", "external_url": null}} -------------------------------------------------------------------------------- /spec/fixtures/user_search.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"username": "shayne", "full_name": "Shayne Sweeney", "type": "user", "id": 20}]} -------------------------------------------------------------------------------- /spec/fixtures/block_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "none" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/deny_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "none" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/unblock_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "none" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/approve_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "follows" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/follow_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "requested" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/unfollow_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "none" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/location_search_fsq.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"latitude": 40.719607, "longitude": -73.986764, "id": "1075772", "name": "Schiller's Liquor Bar"}]} -------------------------------------------------------------------------------- /spec/fixtures/location.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"latitude": 37.780885099999999, "longitude": -122.3948632, "id": 514276, "street_address": "164 south park", "name": "Instagram"}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .DS_Store 4 | .bundle 5 | .rvmrc 6 | .ruby-version 7 | .yardoc 8 | .rake_tasks~ 9 | Gemfile.lock 10 | coverage/* 11 | doc/* 12 | log/* 13 | pkg/* 14 | .idea/* -------------------------------------------------------------------------------- /spec/fixtures/mikeyk.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"username": "mikeyk", "full_name": "Mike Krieger", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292743625.jpg", "id": 4}} -------------------------------------------------------------------------------- /spec/fixtures/shayne.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"username": "shayne", "full_name": "Shayne Sweeney", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_20_75sq_1290558237.jpg", "id": 20}} -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --tag format:"Supported formats" 4 | --tag authenticated:"Requires Authentication" 5 | --tag rate_limited:"Rate Limited" 6 | --markup markdown 7 | - 8 | HISTORY.mkd 9 | LICENSE.mkd -------------------------------------------------------------------------------- /spec/fixtures/media_comment.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"created_time": "1296772367", "text": "hi there", "from": {"username": "shayne", "full_name": "Shayne Sweeney", "type": "user", "id": "20"}, "id": "1862278"}} -------------------------------------------------------------------------------- /spec/fixtures/relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "none", 7 | "incoming_status": "requested_by" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/access_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "at", 3 | "user": { 4 | "username": "mikeyk", 5 | "full_name": "Mike Krieger!!", 6 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 7 | "id": "4" 8 | } 9 | } -------------------------------------------------------------------------------- /spec/fixtures/subscription.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "object": "user", 7 | "type": "subscription", 8 | "id": "1", 9 | "aspect": "media", 10 | "callback_url": "http://your-callback.com/url/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/fixtures/requested_by.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "username": "shayne", 8 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_20_75sq_1295492590_debug.jpg", 9 | "id": "20" 10 | }] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/subscription_payload.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "subscription_id": "1", 3 | "object": "user", 4 | "object_id": "1234", 5 | "changed_aspect": "media", 6 | "time": 1297286541 7 | }, 8 | { 9 | "subscription_id": "2", 10 | "object": "tag", 11 | "object_id": "nofilter", 12 | "changed_aspect": "media", 13 | "time": 1297286541 14 | }] -------------------------------------------------------------------------------- /spec/fixtures/oembed.json: -------------------------------------------------------------------------------- 1 | { 2 | "provider_url": "http:\/\/instagram.com\/", 3 | "media_id": "123657555223544123_41812344", 4 | "title": "I like this title #hash", 5 | "url": "http:\/\/distilleryimage4.s3.amazonaws.com\/7.jpg", 6 | "author_name": "my_name", 7 | "height": 612, 8 | "width": 612, 9 | "version": "1.0", 10 | "author_url": "http:\/\/instagram.com\/", 11 | "author_id": 1234, 12 | "type": "photo", 13 | "provider_name": "Instagram" 14 | } 15 | -------------------------------------------------------------------------------- /spec/fixtures/media_likes.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"username": "chris", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_19_75sq_1286500536.jpg", "id": "19"}, {"username": "kevin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1286592842.jpg", "id": "3"}, {"username": "mikeyk", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1286335374.jpg", "id": "4"}, {"username": "nicole", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_6_75sq_1285365377.jpg", "id": "6"}]} -------------------------------------------------------------------------------- /spec/fixtures/media_comments.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"created_time": "1281045379", "text": "Vet visit", "from": {"username": "doug", "full_name": "Doug Systrom", "type": "user", "id": "17"}, "id": "924"}, {"created_time": "1281046691", "text": "Oh noes!", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": "3"}, "id": "928"}, {"created_time": "1281048012", "text": "I love the fact that even though you can't see Barrett's face, you know he's furklempting by the ears.", "from": {"username": "diane", "full_name": "Diane Systrom", "type": "user", "id": "37"}, "id": "934"}]} -------------------------------------------------------------------------------- /spec/fixtures/subscriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "id": "1", 8 | "type": "subscription", 9 | "object": "user", 10 | "aspect": "media", 11 | "callback_url": "http://your-callback.com/url/" 12 | }, 13 | { 14 | "id": "2", 15 | "type": "subscription", 16 | "object": "location", 17 | "object_id": "2345", 18 | "aspect": "media", 19 | "callback_url": "http://your-callback.com/url/" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /lib/instagram/response.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | module Response 3 | def self.create( response_hash, ratelimit_hash ) 4 | data = response_hash.data.dup rescue response_hash 5 | data.extend( self ) 6 | data.instance_exec do 7 | %w{pagination meta}.each do |k| 8 | response_hash.public_send(k).tap do |v| 9 | instance_variable_set("@#{k}", v) if v 10 | end 11 | end 12 | ratelimit = ::Hashie::Mash.new(ratelimit_hash) 13 | end 14 | data 15 | end 16 | 17 | attr_reader :pagination 18 | attr_reader :meta 19 | attr_reader :ratelimit 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | 9 | namespace :doc do 10 | begin 11 | require 'yard' 12 | rescue LoadError 13 | # ignore 14 | else 15 | YARD::Rake::YardocTask.new do |task| 16 | task.files = ['HISTORY.mkd', 'LICENSE.mkd', 'lib/**/*.rb'] 17 | task.options = [ 18 | '--protected', 19 | '--output-dir', 'doc/yard', 20 | '--tag', 'format:Supported formats', 21 | '--tag', 'authenticated:Requires Authentication', 22 | '--tag', 'rate_limited:Rate Limited', 23 | '--markup', 'markdown', 24 | ] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/instagram/client.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | # Wrapper for the Instagram REST API 3 | # 4 | # @note All methods have been separated into modules and follow the same grouping used in http://instagram.com/developer/ 5 | # @see http://instagram.com/developer/ 6 | class Client < API 7 | Dir[File.expand_path('../client/*.rb', __FILE__)].each{|f| require f} 8 | 9 | include Instagram::Client::Utils 10 | 11 | include Instagram::Client::Users 12 | include Instagram::Client::Media 13 | include Instagram::Client::Locations 14 | include Instagram::Client::Geographies 15 | include Instagram::Client::Tags 16 | include Instagram::Client::Comments 17 | include Instagram::Client::Likes 18 | include Instagram::Client::Subscriptions 19 | include Instagram::Client::Embedding 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/instagram/api.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../connection', __FILE__) 2 | require File.expand_path('../request', __FILE__) 3 | require File.expand_path('../oauth', __FILE__) 4 | 5 | module Instagram 6 | # @private 7 | class API 8 | # @private 9 | attr_accessor *Configuration::VALID_OPTIONS_KEYS 10 | 11 | # Creates a new API 12 | def initialize(options={}) 13 | options = Instagram.options.merge(options) 14 | Configuration::VALID_OPTIONS_KEYS.each do |key| 15 | send("#{key}=", options[key]) 16 | end 17 | end 18 | 19 | def config 20 | conf = {} 21 | Configuration::VALID_OPTIONS_KEYS.each do |key| 22 | conf[key] = send key 23 | end 24 | conf 25 | end 26 | 27 | include Connection 28 | include Request 29 | include OAuth 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/instagram.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../instagram/error', __FILE__) 2 | require File.expand_path('../instagram/configuration', __FILE__) 3 | require File.expand_path('../instagram/api', __FILE__) 4 | require File.expand_path('../instagram/client', __FILE__) 5 | require File.expand_path('../instagram/response', __FILE__) 6 | 7 | module Instagram 8 | extend Configuration 9 | 10 | # Alias for Instagram::Client.new 11 | # 12 | # @return [Instagram::Client] 13 | def self.client(options={}) 14 | Instagram::Client.new(options) 15 | end 16 | 17 | # Delegate to Instagram::Client 18 | def self.method_missing(method, *args, &block) 19 | return super unless client.respond_to?(method) 20 | client.send(method, *args, &block) 21 | end 22 | 23 | # Delegate to Instagram::Client 24 | def self.respond_to?(method, include_all=false) 25 | return client.respond_to?(method, include_all) || super 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/instagram/client/utils.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # @private 4 | module Utils 5 | # Returns the raw full response including all headers. Can be used to access the values for 'X-Ratelimit-Limit' and 'X-Ratelimit-Remaining' 6 | # ==== Examples 7 | # 8 | # client = Instagram.client(:access_token => session[:access_token]) 9 | # response = client.utils_raw_response 10 | # remaining = response.headers[:x_ratelimit_remaining] 11 | # limit = response.headers[:x_ratelimit_limit] 12 | # 13 | def utils_raw_response 14 | response = get('users/self/feed',nil, false, true) 15 | response 16 | end 17 | 18 | private 19 | 20 | # Returns the configured user name or the user name of the authenticated user 21 | # 22 | # @return [String] 23 | def get_username 24 | @user_name ||= self.user.username 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/instagram/connection.rb: -------------------------------------------------------------------------------- 1 | require 'faraday_middleware' 2 | Dir[File.expand_path('../../faraday/*.rb', __FILE__)].each{|f| require f} 3 | 4 | module Instagram 5 | # @private 6 | module Connection 7 | private 8 | 9 | def connection(raw=false) 10 | options = { 11 | :headers => {'Accept' => "application/#{format}; charset=utf-8", 'User-Agent' => user_agent}, 12 | :proxy => proxy, 13 | :url => endpoint, 14 | }.merge(connection_options) 15 | 16 | Faraday::Connection.new(options) do |connection| 17 | connection.use FaradayMiddleware::InstagramOAuth2, client_id, access_token 18 | connection.use Faraday::Request::UrlEncoded 19 | connection.use FaradayMiddleware::Mashify unless raw 20 | unless raw 21 | case format.to_s.downcase 22 | when 'json' then connection.use Faraday::Response::ParseJson 23 | end 24 | end 25 | connection.use FaradayMiddleware::RaiseHttpException 26 | connection.adapter(adapter) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/instagram/error.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | # Custom error class for rescuing from all Instagram errors 3 | class Error < StandardError; end 4 | 5 | # Raised when Instagram returns the HTTP status code 400 6 | class BadRequest < Error; end 7 | 8 | # Raised when Instagram returns the HTTP status code 404 9 | class NotFound < Error; end 10 | 11 | # Raised when Instagram returns the HTTP status code 429 12 | class TooManyRequests < Error; end 13 | 14 | # Raised when Instagram returns the HTTP status code 500 15 | class InternalServerError < Error; end 16 | 17 | # Raised when Instagram returns the HTTP status code 502 18 | class BadGateway < Error; end 19 | 20 | # Raised when Instagram returns the HTTP status code 503 21 | class ServiceUnavailable < Error; end 22 | 23 | # Raised when Instagram returns the HTTP status code 504 24 | class GatewayTimeout < Error; end 25 | 26 | # Raised when a subscription payload hash is invalid 27 | class InvalidSignature < Error; end 28 | 29 | # Raised when Instagram returns the HTTP status code 429 30 | class RateLimitExceeded < Error; end 31 | end 32 | -------------------------------------------------------------------------------- /spec/instagram/client_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | it "should connect using the endpoint configuration" do 5 | client = Instagram::Client.new 6 | endpoint = URI.parse(client.endpoint) 7 | connection = client.send(:connection).build_url(nil).to_s 8 | expect(connection).to eq(endpoint.to_s) 9 | end 10 | 11 | it "should not cache the user account across clients" do 12 | stub_get("users/self.json"). 13 | with(:query => {:access_token => "at1"}). 14 | to_return(:body => fixture("shayne.json"), :headers => {:content_type => "application/json; charset=utf-8"}) 15 | client1 = Instagram::Client.new(:access_token => "at1") 16 | expect(client1.send(:get_username)).to eq("shayne") 17 | stub_get("users/self.json"). 18 | with(:query => {:access_token => "at2"}). 19 | to_return(:body => fixture("mikeyk.json"), :headers => {:content_type => "application/json; charset=utf-8"}) 20 | client2 = Instagram::Client.new(:access_token => "at2") 21 | expect(client2.send(:get_username)).to eq("mikeyk") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/instagram/client/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | 7 | before do 8 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :client_ips => '1.2.3.4', :access_token => 'AT') 9 | end 10 | 11 | describe '.utils_raw_response' do 12 | before do 13 | stub_get("users/self/feed.#{format}"). 14 | with(:query => {:access_token => @client.access_token}). 15 | to_return(:body => fixture("user_media_feed.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 16 | end 17 | 18 | before(:each) do 19 | @response = @client.utils_raw_response 20 | end 21 | 22 | it 'return raw data' do 23 | expect(@response).to be_instance_of(Faraday::Response) 24 | end 25 | 26 | it 'response content headers' do 27 | expect(@response).to be_respond_to(:headers) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/instagram/oauth.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | # Defines HTTP request methods 3 | module OAuth 4 | # Return URL for OAuth authorization 5 | def authorize_url(options={}) 6 | options[:response_type] ||= "code" 7 | options[:scope] ||= scope if !scope.nil? && !scope.empty? 8 | options[:redirect_uri] ||= self.redirect_uri 9 | params = authorization_params.merge(options) 10 | connection.build_url("/oauth/authorize/", params).to_s 11 | end 12 | 13 | # Return an access token from authorization 14 | def get_access_token(code, options={}) 15 | options[:grant_type] ||= "authorization_code" 16 | options[:redirect_uri] ||= self.redirect_uri 17 | params = access_token_params.merge(options) 18 | post("/oauth/access_token/", params.merge(:code => code), signature=false, raw=false, unformatted=true, no_response_wrapper=true) 19 | end 20 | 21 | private 22 | 23 | def authorization_params 24 | { 25 | :client_id => client_id 26 | } 27 | end 28 | 29 | def access_token_params 30 | { 31 | :client_id => client_id, 32 | :client_secret => client_secret 33 | } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/instagram/client/embedding.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to embedding 4 | module Embedding 5 | # Returns information about the media associated with the given short link 6 | # 7 | # @overload oembed(url=nil, options={}) 8 | # @param url [String] An instagram short link 9 | # @param options [Hash] A customizable set of options 10 | # @option options [Integer] :maxheight Maximum height of returned media 11 | # @option options [Integer] :maxwidth Maximum width of returned media 12 | # @option options [Integer] :callback A JSON callback to be invoked 13 | # @return [Hashie::Mash] Information about the media associated with given short link 14 | # @example Return information about the media associated with http://instagr.am/p/BUG/ 15 | # Instagram.oembed(http://instagr.am/p/BUG/) 16 | # 17 | # @see http://instagram.com/developer/embedding/#oembed 18 | # @format :json 19 | # @authenticated false 20 | # @rate_limited true 21 | def oembed(*args) 22 | url = args.first 23 | return nil unless url 24 | get("oembed?url=#{url}", {}, false, false, true) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'simplecov' 3 | rescue LoadError 4 | # ignore 5 | else 6 | SimpleCov.start do 7 | add_group 'Instagram', 'lib/instagram' 8 | add_group 'Faraday Middleware', 'lib/faraday' 9 | add_group 'Specs', 'spec' 10 | end 11 | end 12 | 13 | require File.expand_path('../../lib/instagram', __FILE__) 14 | 15 | require 'rspec' 16 | require 'webmock/rspec' 17 | RSpec.configure do |config| 18 | config.include WebMock::API 19 | end 20 | 21 | def a_delete(path) 22 | a_request(:delete, Instagram.endpoint + path) 23 | end 24 | 25 | def a_get(path) 26 | a_request(:get, Instagram.endpoint + path) 27 | end 28 | 29 | def a_post(path) 30 | a_request(:post, Instagram.endpoint + path) 31 | end 32 | 33 | def a_put(path) 34 | a_request(:put, Instagram.endpoint + path) 35 | end 36 | 37 | def stub_delete(path) 38 | stub_request(:delete, Instagram.endpoint + path) 39 | end 40 | 41 | def stub_get(path) 42 | stub_request(:get, Instagram.endpoint + path) 43 | end 44 | 45 | def stub_post(path) 46 | stub_request(:post, Instagram.endpoint + path) 47 | end 48 | 49 | def stub_put(path) 50 | stub_request(:put, Instagram.endpoint + path) 51 | end 52 | 53 | def fixture_path 54 | File.expand_path("../fixtures", __FILE__) 55 | end 56 | 57 | def fixture(file) 58 | File.new(fixture_path + '/' + file) 59 | end 60 | -------------------------------------------------------------------------------- /PATENTS.md: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights 2 | 3 | "Software" means the Instagram Ruby Gem software distributed by Facebook, Inc. 4 | 5 | Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive, 6 | irrevocable (subject to the termination provision below) license under any 7 | rights in any patent claims owned by Facebook, to make, have made, use, sell, 8 | offer to sell, import, and otherwise transfer the Software. For avoidance of 9 | doubt, no license is granted under Facebook’s rights in any patent claims that 10 | are infringed by (i) modifications to the Software made by you or a third party, 11 | or (ii) the Software in combination with any software or other technology 12 | provided by you or a third party. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | for anyone that makes any claim (including by filing any lawsuit, assertion or 16 | other action) alleging (a) direct, indirect, or contributory infringement or 17 | inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or 18 | affiliates, whether or not such claim is related to the Software, (ii) by any 19 | party if such claim arises in whole or in part from any software, product or 20 | service of Facebook or any of its subsidiaries or affiliates, whether or not 21 | such claim is related to the Software, or (iii) by any party relating to the 22 | Software; or (b) that any right in any patent claim of Facebook is invalid or 23 | unenforceable. -------------------------------------------------------------------------------- /lib/instagram/client/geographies.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to real-time geographies 4 | module Geographies 5 | # Returns a list of recent media items for a given real-time geography 6 | # 7 | # @overload geography_recent_media(id, options={}) 8 | # @param user [Integer] A geography ID from a real-time subscription. 9 | # @param options [Hash] A customizable set of options. 10 | # @option options [Integer] :count (nil) Limit the number of results returned 11 | # @option options [Integer] :min_id (nil) Return media before this min_id 12 | # @option options [Integer] :max_id (nil) Return media after this max_id 13 | # @option options [Integer] :min_timestamp (nil) Return media after this UNIX timestamp 14 | # @option options [Integer] :max_timestamp (nil) Return media before this UNIX timestamp 15 | # @return [Hashie::Mash] 16 | # @example Return a list of the most recent media items taken within a specific geography 17 | # Instagram.geography_recent_media(514276) 18 | # @see http://instagram.com/developer/endpoints/geographies/ 19 | # @format :json 20 | # @authenticated false 21 | # @rate_limited true 22 | def geography_recent_media(id, *args) 23 | options = args.last.is_a?(Hash) ? args.pop : {} 24 | response = get("geographies/#{id}/media/recent", options) 25 | response 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/faraday/oauth2.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | # @private 4 | module FaradayMiddleware 5 | # @private 6 | class InstagramOAuth2 < Faraday::Middleware 7 | def call(env) 8 | 9 | if env[:method] == :get or env[:method] == :delete 10 | if env[:url].query.nil? 11 | query = {} 12 | else 13 | query = Faraday::Utils.parse_query(env[:url].query) 14 | end 15 | 16 | if @access_token and not query["client_secret"] 17 | env[:url].query = Faraday::Utils.build_query(query.merge(:access_token => @access_token)) 18 | env[:request_headers] = env[:request_headers].merge('Authorization' => "Token token=\"#{@access_token}\"") 19 | elsif @client_id 20 | env[:url].query = Faraday::Utils.build_query(query.merge(:client_id => @client_id)) 21 | end 22 | else 23 | if @access_token and not env[:body] && env[:body][:client_secret] 24 | env[:body] = {} if env[:body].nil? 25 | env[:body] = env[:body].merge(:access_token => @access_token) 26 | env[:request_headers] = env[:request_headers].merge('Authorization' => "Token token=\"#{@access_token}\"") 27 | elsif @client_id 28 | env[:body] = env[:body].merge(:client_id => @client_id) 29 | end 30 | end 31 | 32 | 33 | @app.call env 34 | end 35 | 36 | def initialize(app, client_id, access_token=nil) 37 | @app = app 38 | @client_id = client_id 39 | @access_token = access_token 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/instagram/client/embedding_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | describe ".oembed" do 11 | before do 12 | stub_get("oembed"). 13 | with(:query => {:access_token => @client.access_token, :url => "http://instagram.com/p/abcdef"}). 14 | to_return(:body => fixture("oembed.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 15 | end 16 | 17 | it "should get the correct resource" do 18 | @client.oembed("http://instagram.com/p/abcdef") 19 | expect(a_get("oembed?url=http://instagram.com/p/abcdef"). 20 | with(:query => {:access_token => @client.access_token})). 21 | to have_been_made 22 | end 23 | 24 | it "should return the oembed information for an instagram media url" do 25 | oembed = @client.oembed("http://instagram.com/p/abcdef") 26 | expect(oembed.media_id).to eq("123657555223544123_41812344") 27 | end 28 | 29 | it "should return nil if a URL is not provided" do 30 | oembed = @client.oembed 31 | expect(oembed).to be_nil 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/instagram/client/geography_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | 11 | describe ".geography_recent_media" do 12 | 13 | context "with geography ID passed" do 14 | 15 | before do 16 | stub_get("geographies/12345/media/recent.#{format}"). 17 | with(:query => {:access_token => @client.access_token}). 18 | to_return(:body => fixture("geography_recent_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 19 | end 20 | 21 | it "should get the correct resource" do 22 | @client.geography_recent_media(12345) 23 | expect(a_get("geographies/12345/media/recent.#{format}"). 24 | with(:query => {:access_token => @client.access_token})). 25 | to have_been_made 26 | end 27 | 28 | it "should return a list of recent media items within the specifed geography" do 29 | recent_media = @client.geography_recent_media(12345) 30 | expect(recent_media).to be_a Array 31 | expect(recent_media.first.user.username).to eq("amandavan") 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Ruby Instagram Gem software 4 | 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /spec/fixtures/media.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": {"type": 1, "comments": [{"created_time": "2011-01-20T12:05:13+0000", "message": "#youknowitslate when the cab driver wishes you good morning", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 20757161}, {"created_time": "2011-01-20T14:52:12+0000", "message": "Nice", "from": {"username": "newyorkcity", "full_name": "nyc ", "type": "user", "id": 1483611}, "id": 20808205}, {"created_time": "2011-01-20T18:50:02+0000", "message": "I hope you guys got some good work done :)", "from": {"username": "abelnation", "full_name": "Abel Allison", "type": "user", "id": 5315}, "id": 20873301}, {"created_time": "2011-01-20T20:54:21+0000", "message": "Hey do you follow @docpop ?Him, and his friend, made a pretty awesome Instagram Scarf.", "from": {"username": "jasonsposa", "full_name": "jason sposa", "type": "user", "id": 102516}, "id": 20900554}], "caption": {"created_time": "2011-01-20T12:05:13+0000", "message": "#youknowitslate when the cab driver wishes you good morning", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 20757161}, "like_count": 52, "link": "http://api_privatebeta.instagr.am/p/BG9It/", "user": {"username": "mikeyk", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292743625.jpg", "id": 4}, "created_time": "2011-01-20T12:04:54+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_7.jpg", "width": 612, "height": 612}}, "user_has_liked": true, "id": 18600493, "location": null}} -------------------------------------------------------------------------------- /lib/faraday/raise_http_exception.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | 3 | # @private 4 | module FaradayMiddleware 5 | # @private 6 | class RaiseHttpException < Faraday::Middleware 7 | def call(env) 8 | @app.call(env).on_complete do |response| 9 | case response[:status].to_i 10 | when 400 11 | raise Instagram::BadRequest, error_message_400(response) 12 | when 404 13 | raise Instagram::NotFound, error_message_400(response) 14 | when 429 15 | raise Instagram::TooManyRequests, error_message_400(response) 16 | when 500 17 | raise Instagram::InternalServerError, error_message_500(response, "Something is technically wrong.") 18 | when 502 19 | raise Instagram::BadGateway, error_message_500(response, "The server returned an invalid or incomplete response.") 20 | when 503 21 | raise Instagram::ServiceUnavailable, error_message_500(response, "Instagram is rate limiting your requests.") 22 | when 504 23 | raise Instagram::GatewayTimeout, error_message_500(response, "504 Gateway Time-out") 24 | end 25 | end 26 | end 27 | 28 | def initialize(app) 29 | super app 30 | @parser = nil 31 | end 32 | 33 | private 34 | 35 | def error_message_400(response) 36 | "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}#{error_body(response[:body])}" 37 | end 38 | 39 | def error_body(body) 40 | # body gets passed as a string, not sure if it is passed as something else from other spots? 41 | if not body.nil? and not body.empty? and body.kind_of?(String) 42 | # removed multi_json thanks to wesnolte's commit 43 | body = ::JSON.parse(body) 44 | end 45 | 46 | if body.nil? 47 | nil 48 | elsif body['meta'] and body['meta']['error_message'] and not body['meta']['error_message'].empty? 49 | ": #{body['meta']['error_message']}" 50 | elsif body['error_message'] and not body['error_message'].empty? 51 | ": #{body['error_type']}: #{body['error_message']}" 52 | end 53 | end 54 | 55 | def error_message_500(response, body=nil) 56 | "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', body].compact.join(' ')}" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/instagram/client/likes.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to likes 4 | module Likes 5 | # Returns a list of users who like a given media item ID 6 | # 7 | # @overload media_likes(id) 8 | # @param media [Integer] An Instagram media item ID 9 | # @return [Hashie::Mash] A list of users. 10 | # @example Returns a list of users who like the media item of ID 1234 11 | # Instagram.media_likes(777) 12 | # @format :json 13 | # @authenticated true 14 | # 15 | # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). 16 | # @rate_limited true 17 | # @see http://instagram.com/developer/endpoints/likes/#get_media_likes 18 | def media_likes(id, *args) 19 | response = get("media/#{id}/likes") 20 | response 21 | end 22 | 23 | # Issues a like by the currently authenticated user, for a given media item ID 24 | # 25 | # @overload like_media(id, text) 26 | # @param id [Integer] An Instagram media item ID 27 | # @return [Hashie::Mash] Metadata 28 | # @example Like media item with ID 777 29 | # Instagram.like_media(777) 30 | # @format :json 31 | # @authenticated true 32 | # 33 | # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). 34 | # @rate_limited true 35 | # @see http://instagram.com/developer/endpoints/likes/#post_likes 36 | def like_media(id, options={}) 37 | response = post("media/#{id}/likes", options, signature=true) 38 | response 39 | end 40 | 41 | # Removes the like on a givem media item ID for the currently authenticated user 42 | # 43 | # @overload unlike_media(id) 44 | # @param media_id [Integer] An Instagram media item ID. 45 | # @return [Hashie::Mash] Metadata 46 | # @example Remove the like for the currently authenticated user on the media item with the ID of 777 47 | # Instagram.unlike_media(777) 48 | # @format :json 49 | # @authenticated true 50 | # @rate_limited true 51 | # @see http://instagram.com/developer/endpoints/likes/#delete_likes 52 | def unlike_media(id, options={}) 53 | response = delete("media/#{id}/likes", options, signature=true) 54 | response 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /instagram.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/instagram/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.add_development_dependency('rake', '~> 0.9.2.2') 6 | s.add_development_dependency('rspec', '~> 3.1.0') 7 | s.add_development_dependency('webmock', '~> 1.6') 8 | s.add_development_dependency('bluecloth', '~> 2.2.0') 9 | s.add_runtime_dependency('faraday', ['>= 0.7', '< 0.10']) 10 | s.add_runtime_dependency('faraday_middleware', ['>= 0.8', '< 0.10']) 11 | s.add_runtime_dependency('multi_json', '>= 1.0.3', '~> 1.0') 12 | s.add_runtime_dependency('hashie', '>= 0.4.0') 13 | s.authors = ["Shayne Sweeney"] 14 | s.description = %q{A Ruby wrapper for the Instagram REST and Search APIs} 15 | s.post_install_message =<= 1.3.6') if s.respond_to? :required_rubygems_version= 46 | s.rubyforge_project = s.name 47 | s.summary = %q{Ruby wrapper for the Instagram API} 48 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 49 | s.version = Instagram::VERSION.dup 50 | end 51 | -------------------------------------------------------------------------------- /spec/instagram/request_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Request do 4 | describe "#post" do 5 | before do 6 | @ips = "1.2.3.4" 7 | @secret = "CS" 8 | digest = OpenSSL::Digest.new('sha256') 9 | signature = OpenSSL::HMAC.hexdigest(digest, @secret, @ips) 10 | @signed_header = [@ips, signature].join('|') 11 | end 12 | 13 | context "with signature=true" do 14 | it "should set X-Insta-Forwarded-For header" do 15 | client = Instagram::Client.new(:client_id => "CID", :client_secret => @secret, :client_ips => @ips, :access_token => "AT") 16 | url = client.send(:connection).build_url("/media/123/likes.json").to_s 17 | stub_request(:post, url). 18 | with(:body => {"access_token"=>"AT"}). 19 | to_return(:status => 200, :body => "", :headers => {}) 20 | 21 | client.post("/media/123/likes", {}, signature=true) 22 | expect(a_request(:post, url). 23 | with(:headers => {'X-Insta-Forwarded-For'=> @signed_header})). 24 | to have_been_made 25 | end 26 | 27 | it "should not set X-Insta-Fowarded-For header if client_ips is not provided" do 28 | client = Instagram::Client.new(:client_id => "CID", :client_secret => @secret, :access_token => "AT") 29 | url = client.send(:connection).build_url("/media/123/likes.json").to_s 30 | stub_request(:post, url). 31 | with(:body => {"access_token"=>"AT"}). 32 | to_return(:status => 200, :body => "", :headers => {}) 33 | 34 | client.post("/media/123/likes", {}, signature=true) 35 | expect(a_request(:post, url). 36 | with(:headers => {'X-Insta-Forwarded-For'=> @signed_header})). 37 | not_to have_been_made 38 | end 39 | end 40 | 41 | context "with signature=false" do 42 | it "should set X-Insta-Forwarded-For header" do 43 | client = Instagram::Client.new(:client_id => "CID", :client_secret => @secret, :client_ips => @ips, :access_token => "AT") 44 | url = client.send(:connection).build_url("/media/123/likes.json").to_s 45 | stub_request(:post, url). 46 | with(:body => {"access_token"=>"AT"}). 47 | to_return(:status => 200, :body => "", :headers => {}) 48 | 49 | client.post("/media/123/likes", {}, signature=false) 50 | expect(a_request(:post, url). 51 | with(:headers => {'X-Insta-Forwarded-For'=> @signed_header})). 52 | not_to have_been_made 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/instagram/request.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'base64' 3 | 4 | module Instagram 5 | # Defines HTTP request methods 6 | module Request 7 | # Perform an HTTP GET request 8 | def get(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper()) 9 | request(:get, path, options, signature, raw, unformatted, no_response_wrapper) 10 | end 11 | 12 | # Perform an HTTP POST request 13 | def post(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper()) 14 | request(:post, path, options, signature, raw, unformatted, no_response_wrapper) 15 | end 16 | 17 | # Perform an HTTP PUT request 18 | def put(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper()) 19 | request(:put, path, options, signature, raw, unformatted, no_response_wrapper) 20 | end 21 | 22 | # Perform an HTTP DELETE request 23 | def delete(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper()) 24 | request(:delete, path, options, signature, raw, unformatted, no_response_wrapper) 25 | end 26 | 27 | private 28 | 29 | # Perform an HTTP request 30 | def request(method, path, options, signature=false, raw=false, unformatted=false, no_response_wrapper=false) 31 | response = connection(raw).send(method) do |request| 32 | path = formatted_path(path) unless unformatted 33 | case method 34 | when :get, :delete 35 | request.url(URI.encode(path), options) 36 | when :post, :put 37 | request.path = URI.encode(path) 38 | request.body = options unless options.empty? 39 | end 40 | if signature && client_ips != nil 41 | request.headers["X-Insta-Forwarded-For"] = get_insta_fowarded_for(client_ips, client_secret) 42 | end 43 | end 44 | return response if raw 45 | return response.body if no_response_wrapper 46 | return Response.create( response.body, {:limit => response.headers['x-ratelimit-limit'].to_i, 47 | :remaining => response.headers['x-ratelimit-remaining'].to_i} ) 48 | end 49 | 50 | def formatted_path(path) 51 | [path, format].compact.join('.') 52 | end 53 | 54 | def get_insta_fowarded_for(ips, secret) 55 | digest = OpenSSL::Digest.new('sha256') 56 | signature = OpenSSL::HMAC.hexdigest(digest, secret, ips) 57 | return [ips, signature].join('|') 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/instagram/client/likes_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | 7 | before do 8 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :client_ips => '1.2.3.4', :access_token => 'AT') 9 | end 10 | 11 | describe ".media_likes" do 12 | 13 | before do 14 | stub_get("media/777/likes.#{format}"). 15 | with(:query => {:access_token => @client.access_token}). 16 | to_return(:body => fixture("media_likes.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 17 | end 18 | 19 | it "should get the correct resource" do 20 | @client.media_likes(777) 21 | expect(a_get("media/777/likes.#{format}"). 22 | with(:query => {:access_token => @client.access_token})). 23 | to have_been_made 24 | end 25 | 26 | it "should return an array of user search results" do 27 | comments = @client.media_likes(777) 28 | expect(comments).to be_a Array 29 | expect(comments.first.username).to eq("chris") 30 | end 31 | end 32 | 33 | describe ".like_media" do 34 | 35 | before do 36 | stub_post("media/777/likes.#{format}"). 37 | with(:body => {:access_token => @client.access_token}). 38 | to_return(:body => fixture("media_liked.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 39 | end 40 | 41 | it "should get the correct resource" do 42 | @client.like_media(777) 43 | expect(a_post("media/777/likes.#{format}"). 44 | with(:body => {:access_token => @client.access_token})). 45 | to have_been_made 46 | end 47 | end 48 | 49 | describe ".unlike_media" do 50 | 51 | before do 52 | stub_delete("media/777/likes.#{format}"). 53 | with(:query => {:access_token => @client.access_token}). 54 | to_return(:body => fixture("media_unliked.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 55 | end 56 | 57 | it "should get the correct resource" do 58 | @client.unlike_media(777) 59 | expect(a_delete("media/777/likes.#{format}"). 60 | with(:query => {:access_token => @client.access_token})). 61 | to have_been_made 62 | end 63 | end 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /lib/instagram/client/tags.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to tags 4 | module Tags 5 | # Returns extended information of a given Instagram tag 6 | # 7 | # @overload tag(tag) 8 | # @param tag [String] An Instagram tag name 9 | # @return [Hashie::Mash] The requested tag. 10 | # @example Return extended information for the tag "cat" 11 | # Instagram.tag('cat') 12 | # @format :json 13 | # @authenticated false 14 | # @rate_limited true 15 | # @see http://instagram.com/developer/endpoints/tags/#get_tags 16 | def tag(tag, *args) 17 | response = get("tags/#{tag}") 18 | response 19 | end 20 | 21 | # Returns a list of recent media items for a given Instagram tag 22 | # 23 | # @overload tag_recent_media(tag, options={}) 24 | # @param tag-name [String] An Instagram tag name. 25 | # @param options [Hash] A customizable set of options. 26 | # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. 27 | # @option options [Integer] :min_id (nil) Returns results with an ID greater than (that is, newer than) or equal to the specified ID. 28 | # @return [Hashie::Mash] 29 | # @example Return a list of the most recent media items tagged "cat" 30 | # Instagram.tag_recent_media('cat') 31 | # @see http://instagram.com/developer/endpoints/tags/#get_tags_media_recent 32 | # @format :json 33 | # @authenticated false 34 | # @rate_limited true 35 | def tag_recent_media(id, *args) 36 | options = args.last.is_a?(Hash) ? args.pop : {} 37 | response = get("tags/#{id}/media/recent", options, false, false, false) 38 | response 39 | end 40 | 41 | # Returns a list of tags starting with the given search query 42 | # 43 | # @format :json 44 | # @authenticated false 45 | # @rate_limited true 46 | # @param query [String] The beginning or complete tag name to search for 47 | # @param options [Hash] A customizable set of options. 48 | # @option options [Integer] :count The number of media items to retrieve. 49 | # @return [Hashie::Mash] 50 | # @see http://instagram.com/developer/endpoints/tags/#get_tags_search 51 | # @example Return tags that start with "cat" 52 | # Instagram.tag_search("cat") 53 | def tag_search(query, options={}) 54 | response = get('tags/search', options.merge(:q => query)) 55 | response 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/fixtures/location_search.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"latitude": 37.780885099999999, "longitude": -122.3948632, "id": 514276, "street_address": "164 south park", "name": "Instagram"}, {"latitude": 37.780885099999999, "longitude": -122.3948632, "id": 855655, "street_address": "164 South Park", "name": "Instagram Popular"}, {"latitude": 37.781035000000003, "longitude": -122.394758, "id": 172638, "street_address": "164 S Park st", "name": "1 Block Off the Grid"}, {"latitude": 37.780710599999999, "longitude": -122.395044, "id": 544472, "street_address": "551 3rd St", "name": "Shell - South Park"}, {"latitude": 37.780777999999998, "longitude": -122.395123, "id": 81928, "street_address": "", "name": "Lionside"}, {"latitude": 37.781090273546099, "longitude": -122.39529669284821, "id": 5564, "street_address": "521 3rd Street", "name": "HRD Coffee Shop"}, {"latitude": 37.781300000000002, "longitude": -122.395, "id": 620883, "street_address": "164 South Park St.", "name": "Dipity"}, {"latitude": 37.780994877834033, "longitude": -122.3943257331848, "id": 1315, "street_address": "155A South Park", "name": "The Butler & The Chef Bistro"}, {"latitude": 37.780892999999999, "longitude": -122.394211, "id": 13389, "street_address": "155A Southpark st", "name": "The Buttler & The Chef"}, {"latitude": 37.781045300000002, "longitude": -122.3955301, "id": 91185, "street_address": "524 3rd Street", "name": "City Picture Frame"}, {"latitude": 37.780500938066929, "longitude": -122.3943203687668, "id": 26404, "street_address": "599 Third Street", "name": "Foodspotting HQ"}, {"latitude": 37.781074599999997, "longitude": -122.39556690000001, "id": 385659, "street_address": "520 3rd St", "name": "Beer Robot"}, {"latitude": 37.780899481998837, "longitude": -122.3956453800201, "id": 2485, "street_address": "520 3rd Street, Third Floor", "name": "Wired Magazine"}, {"latitude": 37.780450000000002, "longitude": -122.39425, "id": 605764, "street_address": "599 third street", "name": "Rantanplan"}, {"latitude": 37.780449999999988, "longitude": -122.39425, "id": 94183, "street_address": "599 Third Street", "name": "The Yarn Barn"}, {"latitude": 37.781145000000002, "longitude": -122.395678, "id": 945338, "street_address": "500 3rd St", "name": "CMG"}, {"latitude": 37.78156301048017, "longitude": -122.39435791969299, "id": 315279, "street_address": "108 S Park Ave", "name": "South Park Cafe"}, {"latitude": 37.781157, "longitude": -122.393885, "id": 65494, "street_address": "123 South Park St", "name": "PUBLIC Bikes"}, {"latitude": 37.781026676418414, "longitude": -122.3959028720856, "id": 1443, "street_address": "520 3rd Street, Third Floor", "name": "Wired Digital"}, {"latitude": 37.780141, "longitude": -122.394345, "id": 49217, "street_address": "599 3rd Street", "name": "599 3rd St"}]} -------------------------------------------------------------------------------- /spec/faraday/response_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe Faraday::Response do 4 | before do 5 | @client = Instagram::Client.new 6 | end 7 | 8 | { 9 | 400 => Instagram::BadRequest, 10 | 404 => Instagram::NotFound, 11 | 429 => Instagram::TooManyRequests, 12 | 500 => Instagram::InternalServerError, 13 | 503 => Instagram::ServiceUnavailable 14 | }.each do |status, exception| 15 | context "when HTTP status is #{status}" do 16 | 17 | before do 18 | stub_get('users/self/feed.json'). 19 | to_return(:status => status) 20 | end 21 | 22 | it "should raise #{exception.name} error" do 23 | expect { @client.user_media_feed }.to raise_error { exception } 24 | end 25 | end 26 | end 27 | 28 | context "when a 400 is raised" do 29 | before do 30 | stub_get('users/self/feed.json'). 31 | to_return(:body => '{"meta":{"error_message": "Bad words are bad."}}', :status => 400) 32 | end 33 | 34 | it "should return the body error message" do 35 | expect { @client.user_media_feed }.to raise_error(Instagram::BadRequest, /Bad words are bad\./) 36 | end 37 | end 38 | 39 | context "when a 400 is raised with no meta but an error_message" do 40 | before do 41 | stub_get('users/self/feed.json'). 42 | to_return(:body => '{"error_type": "OAuthException", "error_message": "No matching code found."}', :status => 400) 43 | end 44 | 45 | it "should return the body error type and message" do 46 | expect { @client.user_media_feed }.to raise_error(Instagram::BadRequest, /OAuthException: No matching code found\./) 47 | end 48 | end 49 | 50 | context 'when a 502 is raised with an HTML response' do 51 | before do 52 | stub_get('users/self/feed.json').to_return( 53 | :body => '

502 Bad Gateway

The server returned an invalid or incomplete response. ', 54 | :status => 502 55 | ) 56 | end 57 | 58 | it 'should raise an Instagram::BadGateway' do 59 | expect { @client.user_media_feed() }.to raise_error(Instagram::BadGateway) 60 | end 61 | end 62 | 63 | context 'when a 504 is raised with an HTML response' do 64 | before do 65 | stub_get('users/self/feed.json').to_return( 66 | :body => ' 504 Gateway Time-out

504 Gateway Time-out


nginx
', 67 | :status => 504 68 | ) 69 | end 70 | 71 | it 'should raise an Instagram::GatewayTimeout' do 72 | expect { @client.user_media_feed }.to raise_error(Instagram::GatewayTimeout) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/fixtures/media_shortcode.json: -------------------------------------------------------------------------------- 1 | {"meta":{"code":200},"data":{"attribution":null,"tags":["youknowitslate"],"type":"image","location":null,"comments":{"count":3,"data":[{"created_time":"1295535132","text":"Nice","from":{"username":"newyorkcity","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_1483611_75sq_1391632115.jpg","id":"1483611","full_name":"newyorkcity"},"id":"20808205"},{"created_time":"1295549402","text":"I hope you guys got some good work done :)","from":{"username":"abelnation","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_5315_75sq_1391467051.jpg","id":"5315","full_name":"Abel Allison"},"id":"20873301"},{"created_time":"1295556861","text":"Hey do you follow @docpop ?Him, and his friend, made a pretty awesome Instagram Scarf.","from":{"username":"jasonsposa","profile_picture":"http:\/\/photos-c.ak.instagram.com\/hphotos-ak-xaf1\/10616446_1460074390927282_1108706618_a.jpg","id":"102516","full_name":"jason"},"id":"20900554"}]},"filter":"X-Pro II","created_time":"1295525094","link":"http:\/\/instagram.com\/p\/BG9It\/","likes":{"count":52,"data":[{"username":"bailey","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_120_75sq_1328690799.jpg","id":"120","full_name":"Bailey Siewert"},{"username":"bill","profile_picture":"http:\/\/photos-h.ak.instagram.com\/hphotos-ak-xfa1\/10597268_695384913848791_1949499102_a.jpg","id":"34","full_name":"Bill Bogenschutz"},{"username":"juss0445","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_1416773_75sq_1296220025.jpg","id":"1416773","full_name":"juss0445"},{"username":"mimidea","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_1133519_75sq_1361436617.jpg","id":"1133519","full_name":"mimi\ud83d\udc95"}]},"images":{"low_resolution":{"url":"http:\/\/scontent-a.cdninstagram.com\/hphotos-xfa1\/outbound-distillery\/t0.0-17\/OBPTH\/media\/2011\/01\/20\/6248835b0acd48d39d7ee606937ae9f7_6.jpg","width":306,"height":306},"thumbnail":{"url":"http:\/\/scontent-a.cdninstagram.com\/hphotos-xfa1\/outbound-distillery\/t0.0-17\/OBPTH\/media\/2011\/01\/20\/6248835b0acd48d39d7ee606937ae9f7_5.jpg","width":150,"height":150},"standard_resolution":{"url":"http:\/\/scontent-a.cdninstagram.com\/hphotos-xfa1\/outbound-distillery\/t0.0-17\/OBPTH\/media\/2011\/01\/20\/6248835b0acd48d39d7ee606937ae9f7_7.jpg","width":612,"height":612}},"users_in_photo":[],"caption":{"created_time":"1295525094","text":"#youknowitslate when the cab driver wishes you good morning","from":{"username":"mikeyk","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_4_75sq_1374110869.jpg","id":"4","full_name":"Mike Krieger"},"id":"20757161"},"user_has_liked":false,"id":"18600493_4","user":{"username":"mikeyk","website":"","profile_picture":"http:\/\/images.ak.instagram.com\/profiles\/profile_4_75sq_1374110869.jpg","full_name":"Mike Krieger","bio":"","id":"4"}}} 2 | -------------------------------------------------------------------------------- /lib/instagram/client/comments.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to comments 4 | module Comments 5 | # Returns a list of comments for a given media item ID 6 | # 7 | # @overload media_comments(id) 8 | # @param id [Integer] An Instagram media item ID 9 | # @return [Hashie::Mash] The requested comments. 10 | # @example Returns a list of comments for the media item of ID 1234 11 | # Instagram.media_comments(777) 12 | # @format :json 13 | # @authenticated true 14 | # 15 | # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). 16 | # @rate_limited true 17 | # @see http://instagram.com/developer/endpoints/comments/#get_media_comments 18 | def media_comments(id, *args) 19 | response = get("media/#{id}/comments") 20 | response 21 | end 22 | 23 | # Creates a comment for a given media item ID 24 | # 25 | # @overload create_media_comment(id, text) 26 | # @param id [Integer] An Instagram media item ID 27 | # @param text [String] The text of your comment 28 | # @return [Hashie::Mash] The comment created. 29 | # @example Creates a new comment on media item with ID 777 30 | # Instagram.create_media_comment(777, "Oh noes!") 31 | # @format :json 32 | # @authenticated true 33 | # 34 | # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). 35 | # @rate_limited true 36 | # @see http://instagram.com/developer/endpoints/comments/#post_media_comments 37 | def create_media_comment(id, text, options={}) 38 | response = post("media/#{id}/comments", options.merge(:text => text), signature=true) 39 | response 40 | end 41 | 42 | # Deletes a comment for a given media item ID 43 | # 44 | # @overload delete_media_comment(media_id, comment_id) 45 | # @param media_id [Integer] An Instagram media item ID. 46 | # @param comment_id [Integer] Your comment ID of the comment you wish to delete. 47 | # @return [nil] 48 | # @example Delete the comment with ID of 1234, on the media item with ID of 777 49 | # Instagram.delete_media_comment(777, 1234) 50 | # @format :json 51 | # @authenticated true 52 | # 53 | # In order to remove a comment, you must be the owner of the comment, the media item, or both. 54 | # @rate_limited true 55 | # @see http://instagram.com/developer/endpoints/comments/#delete_media_comments 56 | def delete_media_comment(media_id, comment_id, options={}) 57 | response = delete("media/#{media_id}/comments/#{comment_id}", options, signature=true) 58 | response 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/instagram_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | describe Instagram do 4 | after do 5 | Instagram.reset 6 | end 7 | 8 | context "when delegating to a client" do 9 | 10 | before do 11 | stub_get("users/self/feed.json"). 12 | to_return(:body => fixture("user_media_feed.json"), :headers => {:content_type => "application/json; charset=utf-8"}) 13 | end 14 | 15 | it "should get the correct resource" do 16 | Instagram.user_media_feed() 17 | expect(a_get("users/self/feed.json")).to have_been_made 18 | end 19 | 20 | it "should return the same results as a client" do 21 | expect(Instagram.user_media_feed()).to eq(Instagram::Client.new.user_media_feed()) 22 | end 23 | 24 | end 25 | 26 | describe ".client" do 27 | it "should be a Instagram::Client" do 28 | expect(Instagram.client).to be_a Instagram::Client 29 | end 30 | end 31 | 32 | describe ".adapter" do 33 | it "should return the default adapter" do 34 | expect(Instagram.adapter).to eq(Instagram::Configuration::DEFAULT_ADAPTER) 35 | end 36 | end 37 | 38 | describe ".adapter=" do 39 | it "should set the adapter" do 40 | Instagram.adapter = :typhoeus 41 | expect(Instagram.adapter).to eq(:typhoeus) 42 | end 43 | end 44 | 45 | describe ".endpoint" do 46 | it "should return the default endpoint" do 47 | expect(Instagram.endpoint).to eq(Instagram::Configuration::DEFAULT_ENDPOINT) 48 | end 49 | end 50 | 51 | describe ".endpoint=" do 52 | it "should set the endpoint" do 53 | Instagram.endpoint = 'http://tumblr.com' 54 | expect(Instagram.endpoint).to eq('http://tumblr.com') 55 | end 56 | end 57 | 58 | describe ".format" do 59 | it "should return the default format" do 60 | expect(Instagram.format).to eq(Instagram::Configuration::DEFAULT_FORMAT) 61 | end 62 | end 63 | 64 | describe ".format=" do 65 | it "should set the format" do 66 | Instagram.format = 'xml' 67 | expect(Instagram.format).to eq('xml') 68 | end 69 | end 70 | 71 | describe ".user_agent" do 72 | it "should return the default user agent" do 73 | expect(Instagram.user_agent).to eq(Instagram::Configuration::DEFAULT_USER_AGENT) 74 | end 75 | end 76 | 77 | describe ".user_agent=" do 78 | it "should set the user_agent" do 79 | Instagram.user_agent = 'Custom User Agent' 80 | expect(Instagram.user_agent).to eq('Custom User Agent') 81 | end 82 | end 83 | 84 | describe ".configure" do 85 | 86 | Instagram::Configuration::VALID_OPTIONS_KEYS.each do |key| 87 | 88 | it "should set the #{key}" do 89 | Instagram.configure do |config| 90 | config.send("#{key}=", key) 91 | expect(Instagram.send(key)).to eq(key) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/instagram/client/comments_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | 7 | before do 8 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 9 | end 10 | 11 | describe ".media_comments" do 12 | 13 | before do 14 | stub_get("media/777/comments.#{format}"). 15 | with(:query => {:access_token => @client.access_token}). 16 | to_return(:body => fixture("media_comments.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 17 | end 18 | 19 | it "should get the correct resource" do 20 | @client.media_comments(777) 21 | expect(a_get("media/777/comments.#{format}"). 22 | with(:query => {:access_token => @client.access_token})). 23 | to have_been_made 24 | end 25 | 26 | it "should return an array of user search results" do 27 | comments = @client.media_comments(777) 28 | expect(comments).to be_a Array 29 | expect(comments.first.text).to eq("Vet visit") 30 | end 31 | end 32 | 33 | describe ".create_media_comment" do 34 | 35 | before do 36 | stub_post("media/777/comments.#{format}"). 37 | with(:body => {:text => "hi there", :access_token => @client.access_token}). 38 | to_return(:body => fixture("media_comment.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 39 | end 40 | 41 | it "should get the correct resource" do 42 | @client.create_media_comment(777, "hi there") 43 | expect(a_post("media/777/comments.#{format}"). 44 | with(:body => {:text => "hi there", :access_token => @client.access_token})). 45 | to have_been_made 46 | end 47 | 48 | it "should return the new comment when successful" do 49 | comment = @client.create_media_comment(777, "hi there") 50 | expect(comment.text).to eq("hi there") 51 | end 52 | end 53 | 54 | describe ".delete_media_comment" do 55 | 56 | before do 57 | stub_delete("media/777/comments/1234.#{format}"). 58 | with(:query => {:access_token => @client.access_token}). 59 | to_return(:body => fixture("media_comment_deleted.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 60 | end 61 | 62 | it "should get the correct resource" do 63 | @client.delete_media_comment(777, 1234) 64 | expect(a_delete("media/777/comments/1234.#{format}"). 65 | with(:query => {:access_token => @client.access_token})). 66 | to have_been_made 67 | end 68 | end 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /spec/instagram/client/tags_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | describe ".tag" do 11 | 12 | before do 13 | stub_get("tags/cat.#{format}"). 14 | with(:query => {:access_token => @client.access_token}). 15 | to_return(:body => fixture("tag.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 16 | end 17 | 18 | it "should get the correct resource" do 19 | @client.tag('cat') 20 | expect(a_get("tags/cat.#{format}"). 21 | with(:query => {:access_token => @client.access_token})). 22 | to have_been_made 23 | end 24 | 25 | it "should return extended information of a given media item" do 26 | tag = @client.tag('cat') 27 | expect(tag.name).to eq('cat') 28 | end 29 | end 30 | 31 | describe ".tag_recent_media" do 32 | 33 | before do 34 | stub_get("tags/cat/media/recent.#{format}"). 35 | with(:query => {:access_token => @client.access_token}). 36 | to_return(:body => fixture("tag_recent_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 37 | end 38 | 39 | it "should get the correct resource" do 40 | @client.tag_recent_media('cat') 41 | expect(a_get("tags/cat/media/recent.#{format}"). 42 | with(:query => {:access_token => @client.access_token})). 43 | to have_been_made 44 | end 45 | 46 | it "should return a list of media taken at a given location" do 47 | media = @client.tag_recent_media('cat') 48 | expect(media).to be_a Array 49 | expect(media.first.user.username).to eq("amandavan") 50 | end 51 | 52 | end 53 | 54 | describe ".tag_search" do 55 | 56 | before do 57 | stub_get("tags/search.#{format}"). 58 | with(:query => {:access_token => @client.access_token}). 59 | with(:query => {:q => 'cat'}). 60 | to_return(:body => fixture("tag_search.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 61 | end 62 | 63 | it "should get the correct resource" do 64 | @client.tag_search('cat') 65 | expect(a_get("tags/search.#{format}"). 66 | with(:query => {:access_token => @client.access_token}). 67 | with(:query => {:q => 'cat'})). 68 | to have_been_made 69 | end 70 | 71 | it "should return an array of user search results" do 72 | tags = @client.tag_search('cat') 73 | expect(tags).to be_a Array 74 | expect(tags.first.name).to eq("cats") 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/instagram/client/locations.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to media items 4 | module Locations 5 | # Returns extended information of a given Instagram location 6 | # 7 | # @overload location(id) 8 | # @param location [Integer] An Instagram location ID 9 | # @return [Hashie::Mash] The requested location. 10 | # @example Return extended information for the Instagram office 11 | # Instagram.location(514276) 12 | # @format :json 13 | # @authenticated false 14 | # @rate_limited true 15 | # @see http://instagram.com/developer/endpoints/locations/#get_locations 16 | def location(id, *args) 17 | response = get("locations/#{id}") 18 | response 19 | end 20 | 21 | # Returns a list of recent media items for a given Instagram location 22 | # 23 | # @overload location_recent_media(id, options={}) 24 | # @param user [Integer] An Instagram location ID. 25 | # @param options [Hash] A customizable set of options. 26 | # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. 27 | # @option options [Integer] :count (nil) Limits the number of results returned per page. 28 | # @return [Hashie::Mash] 29 | # @example Return a list of the most recent media items taken at the Instagram office 30 | # Instagram.location_recent_media(514276) 31 | # @see http://instagram.com/developer/endpoints/locations/#get_locations_media_recent 32 | # @format :json 33 | # @authenticated false 34 | # @rate_limited true 35 | def location_recent_media(id, *args) 36 | options = args.last.is_a?(Hash) ? args.pop : {} 37 | response = get("locations/#{id}/media/recent", options) 38 | response 39 | end 40 | 41 | # Returns Instagram locations within proximity of given lat,lng or foursquare venue id 42 | # 43 | # @overload location_search(options={}) 44 | # @param foursquare_v2_id [String] A valid Foursquare Venue ID (v2) 45 | # @param lat [String] A given latitude in decimal format 46 | # @param lng [String] A given longitude in decimal format 47 | # @option options [Integer] :count The number of media items to retrieve. 48 | # @return [Hashie::Mash] location resultm object, #data is an Array. 49 | # @example 1: Return a location with the Foursquare Venue ID = () 50 | # Instagram.location_search("3fd66200f964a520c5f11ee3") (Schiller's Liquor Bar, 131 Rivington St., NY, NY 10002) 51 | # @example 2: Return locations around 37.7808851, -122.3948632 (164 S Park, SF, CA USA) 52 | # Instagram.location_search("37.7808851", "-122.3948632") 53 | # @see http://instagram.com/developer/endpoints/locations/#get_locations_search 54 | # @format :json 55 | # @authenticated false 56 | # @rate_limited true 57 | def location_search(*args) 58 | options = args.last.is_a?(Hash) ? args.pop : {} 59 | case args.size 60 | when 1 61 | foursquare_v2_id = args.first 62 | response = get('locations/search', options.merge(:foursquare_v2_id => foursquare_v2_id)) 63 | when 2 64 | lat, lng = args 65 | response = get('locations/search', options.merge(:lat => lat, :lng => lng)) 66 | when 3 67 | lat, lng, distance = args 68 | response = get('locations/search', options.merge(:lat => lat, :lng => lng, :distance => distance)) 69 | end 70 | response 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/instagram/client/media.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to media items 4 | module Media 5 | # Returns extended information of a given media item 6 | # 7 | # @overload media_item(id) 8 | # @param user [Integer] An Instagram media item ID 9 | # @return [Hashie::Mash] The requested media item. 10 | # @example Return extended information for media item 1234 11 | # Instagram.media_item(1324) 12 | # @format :json 13 | # @authenticated false unless requesting media from a protected user 14 | # 15 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 16 | # @rate_limited true 17 | # @see http://instagram.com/developer/endpoints/media/#get_media 18 | def media_item(*args) 19 | id = args.first || 'self' 20 | response = get("media/#{id}") 21 | response 22 | end 23 | 24 | # Returns extended information of a given media item 25 | # 26 | # @overload media_shortcode(shortcode) 27 | # @param shortcode [String] An Instagram media item shortcode 28 | # @return [Hashie::Mash] The requested media item. 29 | # @example Return extended information for media item with shortcode 'D' 30 | # Instagram.media_shortcode('D') 31 | # @format none 32 | # @authenticated false unless requesting media from a protected user 33 | # 34 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 35 | # @rate_limited true 36 | # @see http://instagram.com/developer/endpoints/media/#get_media_by_shortcode 37 | def media_shortcode(*args) 38 | shortcode = args.first 39 | response = get("media/shortcode/#{shortcode}", {}, false, false, true) 40 | response 41 | end 42 | 43 | # Returns a list of the overall most popular media 44 | # 45 | # @overload media_popular(options={}) 46 | # @param options [Hash] A customizable set of options. 47 | # @return [Hashie::Mash] 48 | # @example Returns a list of the overall most popular media 49 | # Instagram.media_popular 50 | # @see http://instagram.com/developer/endpoints/media/#get_media_popular 51 | # @format :json 52 | # @authenticated false unless requesting it from a protected user 53 | # 54 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 55 | # @rate_limited true 56 | def media_popular(*args) 57 | options = args.last.is_a?(Hash) ? args.pop : {} 58 | id = args.first || "self" 59 | response = get("media/popular", options) 60 | response 61 | end 62 | 63 | # Returns media items within proximity of given lat,lng 64 | # 65 | # @param lat [String] A given latitude in decimal format 66 | # @param lng [String] A given longitude in decimal format 67 | # @param options [Hash] A customizable set of options. 68 | # @option options [Integer] :count The number of media items to retrieve. 69 | # @return [Hashie::Mash] A list of matching media 70 | # @example Return media around 37.7808851, -122.3948632 (164 S Park, SF, CA USA) 71 | # Instagram.media_search("37.7808851", "-122.3948632") 72 | # @see http://instagram.com/developer/endpoints/media/#get_media_search 73 | # @format :json 74 | # @authenticated false 75 | # @rate_limited true 76 | def media_search(lat, lng, options={}) 77 | response = get('media/search', options.merge(:lat => lat, :lng => lng)) 78 | response 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/instagram/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require File.expand_path('../version', __FILE__) 3 | 4 | module Instagram 5 | # Defines constants and methods related to configuration 6 | module Configuration 7 | # An array of valid keys in the options hash when configuring a {Instagram::API} 8 | VALID_OPTIONS_KEYS = [ 9 | :access_token, 10 | :adapter, 11 | :client_id, 12 | :client_secret, 13 | :client_ips, 14 | :connection_options, 15 | :scope, 16 | :redirect_uri, 17 | :endpoint, 18 | :format, 19 | :proxy, 20 | :user_agent, 21 | :no_response_wrapper 22 | ].freeze 23 | 24 | # By default, don't set a user access token 25 | DEFAULT_ACCESS_TOKEN = nil 26 | 27 | # The adapter that will be used to connect if none is set 28 | # 29 | # @note The default faraday adapter is Net::HTTP. 30 | DEFAULT_ADAPTER = Faraday.default_adapter 31 | 32 | # By default, don't set an application ID 33 | DEFAULT_CLIENT_ID = nil 34 | 35 | # By default, don't set an application secret 36 | DEFAULT_CLIENT_SECRET = nil 37 | 38 | # By default, don't set application IPs 39 | DEFAULT_CLIENT_IPS = nil 40 | 41 | # By default, don't set any connection options 42 | DEFAULT_CONNECTION_OPTIONS = {} 43 | 44 | # The endpoint that will be used to connect if none is set 45 | # 46 | # @note There is no reason to use any other endpoint at this time 47 | DEFAULT_ENDPOINT = 'https://api.instagram.com/v1/'.freeze 48 | 49 | # The response format appended to the path and sent in the 'Accept' header if none is set 50 | # 51 | # @note JSON is the only available format at this time 52 | DEFAULT_FORMAT = :json 53 | 54 | # By default, don't use a proxy server 55 | DEFAULT_PROXY = nil 56 | 57 | # By default, don't set an application redirect uri 58 | DEFAULT_REDIRECT_URI = nil 59 | 60 | # By default, don't set a user scope 61 | DEFAULT_SCOPE = nil 62 | 63 | # By default, don't wrap responses with meta data (i.e. pagination) 64 | DEFAULT_NO_RESPONSE_WRAPPER = false 65 | 66 | # The user agent that will be sent to the API endpoint if none is set 67 | DEFAULT_USER_AGENT = "Instagram Ruby Gem #{Instagram::VERSION}".freeze 68 | 69 | # An array of valid request/response formats 70 | # 71 | # @note Not all methods support the XML format. 72 | VALID_FORMATS = [ 73 | :json].freeze 74 | 75 | # @private 76 | attr_accessor *VALID_OPTIONS_KEYS 77 | 78 | # When this module is extended, set all configuration options to their default values 79 | def self.extended(base) 80 | base.reset 81 | end 82 | 83 | # Convenience method to allow configuration options to be set in a block 84 | def configure 85 | yield self 86 | end 87 | 88 | # Create a hash of options and their values 89 | def options 90 | VALID_OPTIONS_KEYS.inject({}) do |option, key| 91 | option.merge!(key => send(key)) 92 | end 93 | end 94 | 95 | # Reset all configuration options to defaults 96 | def reset 97 | self.access_token = DEFAULT_ACCESS_TOKEN 98 | self.adapter = DEFAULT_ADAPTER 99 | self.client_id = DEFAULT_CLIENT_ID 100 | self.client_secret = DEFAULT_CLIENT_SECRET 101 | self.client_ips = DEFAULT_CLIENT_IPS 102 | self.connection_options = DEFAULT_CONNECTION_OPTIONS 103 | self.scope = DEFAULT_SCOPE 104 | self.redirect_uri = DEFAULT_REDIRECT_URI 105 | self.endpoint = DEFAULT_ENDPOINT 106 | self.format = DEFAULT_FORMAT 107 | self.proxy = DEFAULT_PROXY 108 | self.user_agent = DEFAULT_USER_AGENT 109 | self.no_response_wrapper= DEFAULT_NO_RESPONSE_WRAPPER 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/fixtures/tag_search.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"description": null, "media_count": 940, "name": "cats", "external_url": null}, {"description": null, "media_count": 5, "name": "catan", "external_url": null}, {"description": null, "media_count": 1, "name": "catch", "external_url": null}, {"description": null, "media_count": 6, "name": "catsg", "external_url": null}, {"description": null, "media_count": 2, "name": "catmug", "external_url": null}, {"description": null, "media_count": 1, "name": "cattoy", "external_url": null}, {"description": null, "media_count": 2, "name": "cattle", "external_url": null}, {"description": null, "media_count": 1, "name": "catlip", "external_url": null}, {"description": null, "media_count": 1, "name": "catwalk", "external_url": null}, {"description": null, "media_count": 55, "name": "catcafe", "external_url": null}, {"description": null, "media_count": 3, "name": "cateyes", "external_url": null}, {"description": null, "media_count": 2, "name": "catnews", "external_url": null}, {"description": null, "media_count": 1, "name": "catfail", "external_url": null}, {"description": null, "media_count": 6, "name": "catgirl", "external_url": null}, {"description": null, "media_count": 1, "name": "catchup", "external_url": null}, {"description": null, "media_count": 1, "name": "catyawn", "external_url": null}, {"description": null, "media_count": 1, "name": "catdang", "external_url": null}, {"description": null, "media_count": 3, "name": "catbath", "external_url": null}, {"description": null, "media_count": 5, "name": "catdong", "external_url": null}, {"description": null, "media_count": 1, "name": "catalog", "external_url": null}, {"description": null, "media_count": 1, "name": "catching", "external_url": null}, {"description": null, "media_count": 1, "name": "catscafe", "external_url": null}, {"description": null, "media_count": 2, "name": "catalina", "external_url": null}, {"description": null, "media_count": 1, "name": "catlanta", "external_url": null}, {"description": null, "media_count": 1, "name": "cattails", "external_url": null}, {"description": null, "media_count": 2, "name": "catering", "external_url": null}, {"description": null, "media_count": 6, "name": "catedral", "external_url": null}, {"description": null, "media_count": 1, "name": "catheads", "external_url": null}, {"description": null, "media_count": 11, "name": "catholic", "external_url": null}, {"description": null, "media_count": 57, "name": "catsrock", "external_url": null}, {"description": null, "media_count": 1, "name": "cattelan", "external_url": null}, {"description": null, "media_count": 9, "name": "catchico", "external_url": null}, {"description": null, "media_count": 55, "name": "cathedral", "external_url": null}, {"description": null, "media_count": 1, "name": "catacombs", "external_url": null}, {"description": null, "media_count": 1, "name": "cattweets", "external_url": null}, {"description": null, "media_count": 2, "name": "catalogue", "external_url": null}, {"description": null, "media_count": 1, "name": "catalonia", "external_url": null}, {"description": null, "media_count": 2, "name": "cat_fight", "external_url": null}, {"description": null, "media_count": 3, "name": "catalunya", "external_url": null}, {"description": null, "media_count": 7, "name": "cattitude", "external_url": null}, {"description": null, "media_count": 2, "name": "cathkidson", "external_url": null}, {"description": null, "media_count": 1, "name": "cathkidston", "external_url": null}, {"description": null, "media_count": 3, "name": "catmolester", "external_url": null}, {"description": null, "media_count": 9, "name": "caterpillar", "external_url": null}, {"description": null, "media_count": 1, "name": "catmomguilt", "external_url": null}, {"description": null, "media_count": 1, "name": "catinthehat", "external_url": null}, {"description": null, "media_count": 2, "name": "catscatscats", "external_url": null}, {"description": null, "media_count": 1, "name": "cataractgorge", "external_url": null}, {"description": null, "media_count": 5, "name": "cathedraloflearning", "external_url": null}, {"description": null, "media_count": 1, "name": "catalacorrectepassal", "external_url": null}]} -------------------------------------------------------------------------------- /spec/instagram/client/media_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | describe ".media_item" do 11 | 12 | before do 13 | stub_get("media/18600493.#{format}"). 14 | with(:query => {:access_token => @client.access_token}). 15 | to_return(:body => fixture("media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 16 | end 17 | 18 | it "should get the correct resource" do 19 | @client.media_item(18600493) 20 | expect(a_get("media/18600493.#{format}"). 21 | with(:query => {:access_token => @client.access_token})). 22 | to have_been_made 23 | end 24 | 25 | it "should return extended information of a given media item" do 26 | media = @client.media_item(18600493) 27 | expect(media.user.username).to eq("mikeyk") 28 | end 29 | end 30 | 31 | describe ".media_shortcode" do 32 | 33 | before do 34 | stub_get('media/shortcode/BG9It'). 35 | with(:query => {:access_token => @client.access_token}). 36 | to_return(:body => fixture("media_shortcode.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 37 | end 38 | 39 | it "should get the correct resource" do 40 | @client.media_shortcode('BG9It') 41 | expect(a_get('media/shortcode/BG9It'). 42 | with(:query => {:access_token => @client.access_token})). 43 | to have_been_made 44 | end 45 | 46 | it "should return extended information of a given media item" do 47 | media = @client.media_shortcode('BG9It') 48 | expect(media.user.username).to eq('mikeyk') 49 | end 50 | end 51 | 52 | describe ".media_popular" do 53 | 54 | before do 55 | stub_get("media/popular.#{format}"). 56 | with(:query => {:access_token => @client.access_token}). 57 | to_return(:body => fixture("media_popular.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 58 | end 59 | 60 | it "should get the correct resource" do 61 | @client.media_popular 62 | expect(a_get("media/popular.#{format}"). 63 | with(:query => {:access_token => @client.access_token})). 64 | to have_been_made 65 | end 66 | 67 | it "should return popular media items" do 68 | media_popular = @client.media_popular 69 | expect(media_popular).to be_a Array 70 | media_popular.first.user.username == "babycamera" 71 | end 72 | end 73 | 74 | describe ".media_search" do 75 | 76 | before do 77 | stub_get("media/search.#{format}"). 78 | with(:query => {:access_token => @client.access_token}). 79 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632"}). 80 | to_return(:body => fixture("media_search.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 81 | end 82 | 83 | it "should get the correct resource" do 84 | @client.media_search("37.7808851", "-122.3948632") 85 | expect(a_get("media/search.#{format}"). 86 | with(:query => {:access_token => @client.access_token}). 87 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632"})). 88 | to have_been_made 89 | end 90 | 91 | it "should return an array of user search results" do 92 | media_search = @client.media_search("37.7808851", "-122.3948632") 93 | expect(media_search).to be_a Array 94 | expect(media_search.first.user.username).to eq("mikeyk") 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/instagram/client/locations_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | describe ".location" do 11 | 12 | before do 13 | stub_get("locations/514276.#{format}"). 14 | with(:query => {:access_token => @client.access_token}). 15 | to_return(:body => fixture("location.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 16 | end 17 | 18 | it "should get the correct resource" do 19 | @client.location(514276) 20 | expect(a_get("locations/514276.#{format}"). 21 | with(:query => {:access_token => @client.access_token})). 22 | to have_been_made 23 | end 24 | 25 | it "should return extended information of a given location" do 26 | location = @client.location(514276) 27 | expect(location.name).to eq("Instagram") 28 | end 29 | end 30 | 31 | describe ".location_recent_media" do 32 | 33 | before do 34 | stub_get("locations/514276/media/recent.#{format}"). 35 | with(:query => {:access_token => @client.access_token}). 36 | to_return(:body => fixture("location_recent_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 37 | end 38 | 39 | it "should get the correct resource" do 40 | @client.location_recent_media(514276) 41 | expect(a_get("locations/514276/media/recent.#{format}"). 42 | with(:query => {:access_token => @client.access_token})). 43 | to have_been_made 44 | end 45 | 46 | it "should return a list of media taken at a given location" do 47 | media = @client.location_recent_media(514276) 48 | expect(media).to be_a Array 49 | expect(media.first.user.username).to eq("josh") 50 | end 51 | end 52 | 53 | describe ".location_search_lat_lng" do 54 | 55 | before do 56 | stub_get("locations/search.#{format}"). 57 | with(:query => {:access_token => @client.access_token}). 58 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632"}). 59 | to_return(:body => fixture("location_search.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 60 | end 61 | 62 | it "should get the correct resource by lat/lng" do 63 | @client.location_search("37.7808851", "-122.3948632") 64 | expect(a_get("locations/search.#{format}"). 65 | with(:query => {:access_token => @client.access_token}). 66 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632"})). 67 | to have_been_made 68 | end 69 | 70 | it "should return an array of user search results" do 71 | locations = @client.location_search("37.7808851", "-122.3948632") 72 | expect(locations).to be_a Array 73 | expect(locations.first.name).to eq("Instagram") 74 | end 75 | end 76 | 77 | describe ".location_search_lat_lng_distance" do 78 | 79 | before do 80 | stub_get("locations/search.#{format}"). 81 | with(:query => {:access_token => @client.access_token}). 82 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632", :distance => "5000"}). 83 | to_return(:body => fixture("location_search.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 84 | end 85 | 86 | it "should get the correct resource by lat/lng/distance" do 87 | @client.location_search("37.7808851", "-122.3948632", "5000") 88 | expect(a_get("locations/search.#{format}"). 89 | with(:query => {:access_token => @client.access_token}). 90 | with(:query => {:lat => "37.7808851", :lng => "-122.3948632", :distance => "5000"})). 91 | to have_been_made 92 | end 93 | 94 | it "should return an array of user search results" do 95 | locations = @client.location_search("37.7808851", "-122.3948632", "5000") 96 | expect(locations).to be_a Array 97 | expect(locations.first.name).to eq("Instagram") 98 | end 99 | end 100 | 101 | describe ".location_search_foursquare_v2_id" do 102 | 103 | before do 104 | stub_get("locations/search.#{format}"). 105 | with(:query => {:access_token => @client.access_token}). 106 | with(:query => {:foursquare_v2_id => "3fd66200f964a520c5f11ee3"}). 107 | to_return(:body => fixture("location_search_fsq.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 108 | end 109 | 110 | it "should get the correct resource by foursquare_v2_id" do 111 | @client.location_search("3fd66200f964a520c5f11ee3") 112 | expect(a_get("locations/search.#{format}"). 113 | with(:query => {:access_token => @client.access_token}). 114 | with(:query => {:foursquare_v2_id => "3fd66200f964a520c5f11ee3"})). 115 | to have_been_made 116 | end 117 | 118 | it "should return an array of user search results" do 119 | locations = @client.location_search("3fd66200f964a520c5f11ee3") 120 | expect(locations).to be_a Array 121 | expect(locations.first.name).to eq("Schiller's Liquor Bar") 122 | end 123 | end 124 | 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/fixtures/follows.json: -------------------------------------------------------------------------------- 1 | {"paging": {"next": "http://api.instagram.com/v1/users/20/follows?access_token=at&q=Shayne+Sweeney&cursor=10906239"}, "meta": {"code": 200}, "data": [{"username": "heartsf", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_814223_75sq_1295678065.jpg", "id": 814223}, {"username": "sbtesol", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1676861}, {"username": "themark42", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1683782}, {"username": "klyons", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1703903_75sq_1296314135.jpg", "id": 1703903}, {"username": "garyvee", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1697296_75sq_1296158123.jpg", "id": 1697296}, {"username": "bizstone", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_41348_75sq_1293327839.jpg", "id": 41348}, {"username": "dangelo", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3290_75sq_1292749774.jpg", "id": 3290}, {"username": "suicidegirls", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1186880_75sq_1295581034.jpg", "id": 1186880}, {"username": "jayzombie", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_95_75sq_1294674528.jpg", "id": 95}, {"username": "thegrammys", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1352742_75sq_1294269333.jpg", "id": 1352742}, {"username": "cnnireport", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1321522_75sq_1294085544.jpg", "id": 1321522}, {"username": "youtube", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1337343_75sq_1295052152.jpg", "id": 1337343}, {"username": "redbull", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_476322_75sq_1288938542.jpg", "id": 476322}, {"username": "rabidsloth", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 975392}, {"username": "nkanemoto", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1119179}, {"username": "svanhout", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1578415_75sq_1295492261.jpg", "id": 1578415}, {"username": "kerryd82", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_465287_75sq_1293301958.jpg", "id": 465287}, {"username": "jalter", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.png", "id": 51}, {"username": "brieanemarie", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1557158_75sq_1295377730.jpg", "id": 1557158}, {"username": "fordryan", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1153542}, {"username": "snoopdogg", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg", "id": 1574083}, {"username": "kamalravikant", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1491754_75sq_1295074336.jpg", "id": 1491754}, {"username": "tenniscrook", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1502560}, {"username": "dougreg", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1424698}, {"username": "maria", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_60_75sq_1286907839.jpg", "id": 60}, {"username": "weldthisone", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1431100}, {"username": "nickb2400", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1118518}, {"username": "sarasiri", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 123324}, {"username": "yarnell", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 108894}, {"username": "atebits", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1294612_75sq_1294105539.jpg", "id": 1294612}, {"username": "julien51", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 115583}, {"username": "starbucks", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1034466_75sq_1293144108.jpg", "id": 1034466}, {"username": "doug", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_17_75sq_1292890348.jpg", "id": 17}, {"username": "nbcnews", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1269598_75sq_1294082789.jpg", "id": 1269598}, {"username": "evospeedracer", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1328298_75sq_1294120971.jpg", "id": 1328298}, {"username": "npr", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1258618_75sq_1293821873.jpg", "id": 1258618}, {"username": "mikeintampa", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1266404}, {"username": "sammienicole", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1262271_75sq_1295626361.jpg", "id": 1262271}, {"username": "mackieleigh", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1262165_75sq_1293816876.jpg", "id": 1262165}, {"username": "ntnl", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_622967_75sq_1289527600.jpg", "id": 622967}, {"username": "crippledpetey", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_908190_75sq_1293602628.jpg", "id": 908190}, {"username": "naveen", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_79_75sq_1284678395.jpg", "id": 79}, {"username": "labusque", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_357541_75sq_1288491953.jpg", "id": 357541}, {"username": "roach0123", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1150120_75sq_1295804260.jpg", "id": 1150120}, {"username": "markmanduca", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1153220_75sq_1293250587.jpg", "id": 1153220}, {"username": "dianeveronica", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_545015_75sq_1289176716.jpg", "id": 545015}, {"username": "leahmariebrooks", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1146978}, {"username": "cassiesweeney", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1137873_75sq_1293162628.jpg", "id": 1137873}, {"username": "parallaxchico", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1139928_75sq_1293175329.jpg", "id": 1139928}, {"username": "om", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2637_75sq_1286977009.jpg", "id": 2637}]} -------------------------------------------------------------------------------- /spec/fixtures/followed_by.json: -------------------------------------------------------------------------------- 1 | {"paging": {"next": "http://api.instagram.com/v1/users/20/followed-by?access_token=at&cursor=19490800"}, "meta": {"code": 200}, "data": [{"username": "bojieyang", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1776468}, {"username": "samanthadelaide", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774434_75sq_1296575655.jpg", "id": 1774434}, {"username": "aericangelo", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_831982_75sq_1291903923.jpg", "id": 831982}, {"username": "arosa13", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_99215_75sq_1295887470.jpg", "id": 99215}, {"username": "prensessa", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_42309_75sq_1286603395.jpg", "id": 42309}, {"username": "hibarista", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1775803_75sq_1296583859.jpg", "id": 1775803}, {"username": "g_e_m", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1770129_75sq_1296548465.jpg", "id": 1770129}, {"username": "stephybear987", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1775658_75sq_1296582616.jpg", "id": 1775658}, {"username": "henshin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1646136}, {"username": "misunkim", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1775480_75sq_1296581465.jpg", "id": 1775480}, {"username": "wikipediakid", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1775261_75sq_1296580075.jpg", "id": 1775261}, {"username": "anmolm", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1775158_75sq_1296579533.jpg", "id": 1775158}, {"username": "5f", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1757297_75sq_1296472161.jpg", "id": 1757297}, {"username": "jmo28", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774978_75sq_1296578473.jpg", "id": 1774978}, {"username": "docfranzke", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1079120_75sq_1294650560.jpg", "id": 1079120}, {"username": "kussarah", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1774841}, {"username": "migup", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_218331_75sq_1291410648.jpg", "id": 218331}, {"username": "keruri", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774807_75sq_1296577696.jpg", "id": 1774807}, {"username": "krittakorn", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774715_75sq_1296577203.jpg", "id": 1774715}, {"username": "saviou", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1774698}, {"username": "jjfoxhound", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774666_75sq_1296576925.jpg", "id": 1774666}, {"username": "henryedaz", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1068785_75sq_1292704467.jpg", "id": 1068785}, {"username": "dottsboo", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774626_75sq_1296576746.jpg", "id": 1774626}, {"username": "gogoheadbritt", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1774542}, {"username": "freakiinkidd13", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1774458}, {"username": "dhd", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774255_75sq_1296574807.jpg", "id": 1774255}, {"username": "ultimatekbox", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1774161_75sq_1296574308.jpg", "id": 1774161}, {"username": "laurelhope", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1773922_75sq_1296574116.jpg", "id": 1773922}, {"username": "fearmytofu", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1756478_75sq_1296466626.jpg", "id": 1756478}, {"username": "crystal_faith", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1773314_75sq_1296569794.jpg", "id": 1773314}, {"username": "jingmaili", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1773236}, {"username": "geeishanaate76", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1773147_75sq_1296568942.jpg", "id": 1773147}, {"username": "lyling82", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1772900}, {"username": "beccanash", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1772861_75sq_1296569585.jpg", "id": 1772861}, {"username": "santah", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1772796_75sq_1296567010.jpg", "id": 1772796}, {"username": "locaflower", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1771328_75sq_1296557612.jpg", "id": 1771328}, {"username": "yrq", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1772320_75sq_1296564304.jpg", "id": 1772320}, {"username": "jamielacerda", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1238760_75sq_1296564659.jpg", "id": 1238760}, {"username": "official_cat", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_218642_75sq_1294783335.jpg", "id": 218642}, {"username": "richurlex", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1771923}, {"username": "sarahleeeleonore", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1771896}, {"username": "faxvaag", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1122606_75sq_1293791250.jpg", "id": 1122606}, {"username": "makistyle", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1738402_75sq_1296370804.jpg", "id": 1738402}, {"username": "cuthers87", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1771277_75sq_1296557172.jpg", "id": 1771277}, {"username": "jadekang", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1771054_75sq_1296555429.jpg", "id": 1771054}, {"username": "thelostbear", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1770958_75sq_1296554737.jpg", "id": 1770958}, {"username": "elf826", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1770898_75sq_1296554267.jpg", "id": 1770898}, {"username": "cameronwarhol", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1770793}, {"username": "krawcurulez", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", "id": 1770765}, {"username": "harrislakers", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1770505_75sq_1296551431.jpg", "id": 1770505}]} -------------------------------------------------------------------------------- /spec/instagram/client/subscriptions_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | 7 | before do 8 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 9 | end 10 | 11 | describe ".subscriptions" do 12 | 13 | before do 14 | stub_get("subscriptions.#{format}"). 15 | with(:query => {:client_id => @client.client_id, :client_secret => @client.client_secret}). 16 | to_return(:body => fixture("subscriptions.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 17 | end 18 | 19 | it "should get the correct resource" do 20 | @client.subscriptions 21 | expect(a_get("subscriptions.#{format}"). 22 | with(:query => {:client_id => @client.client_id, :client_secret => @client.client_secret})). 23 | to have_been_made 24 | end 25 | 26 | it "should return an array of subscriptions" do 27 | subscriptions = @client.subscriptions 28 | expect(subscriptions).to be_a Array 29 | expect(subscriptions.first.object).to eq("user") 30 | end 31 | end 32 | 33 | describe ".create_subscription" do 34 | 35 | before do 36 | stub_post("subscriptions.#{format}"). 37 | with(:body => {:object => "user", :callback_url => "http://example.com/instagram/callback", :aspect => "media", :client_id => @client.client_id, :client_secret => @client.client_secret}). 38 | to_return(:body => fixture("subscription.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 39 | end 40 | 41 | it "should get the correct resource" do 42 | @client.create_subscription("user", :callback_url => "http://example.com/instagram/callback") 43 | expect(a_post("subscriptions.#{format}"). 44 | with(:body => {:object => "user", :callback_url => "http://example.com/instagram/callback", :aspect => "media", :client_id => @client.client_id, :client_secret => @client.client_secret})). 45 | to have_been_made 46 | end 47 | 48 | it "should return the new subscription when successful" do 49 | subscription = @client.create_subscription("user", :callback_url => "http://example.com/instagram/callback") 50 | expect(subscription.object).to eq("user") 51 | end 52 | end 53 | 54 | describe ".delete_media_comment" do 55 | 56 | before do 57 | stub_delete("subscriptions.#{format}"). 58 | with(:query => {:object => "user", :client_id => @client.client_id, :client_secret => @client.client_secret}). 59 | to_return(:body => fixture("subscription_deleted.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 60 | end 61 | 62 | it "should get the correct resource" do 63 | @client.delete_subscription(:object => "user") 64 | expect(a_delete("subscriptions.#{format}"). 65 | with(:query => {:object => "user", :client_id => @client.client_id, :client_secret => @client.client_secret})). 66 | to have_been_made 67 | end 68 | end 69 | 70 | describe ".validate_update" do 71 | 72 | subject { @client.validate_update(body, headers) } 73 | 74 | context "when calculated signature matches request signature" do 75 | 76 | let(:body) { {foo: "bar"}.to_json } 77 | let(:request_signature) { OpenSSL::HMAC.hexdigest('sha1', @client.client_secret, body) } 78 | let(:headers) { {"X-Hub-Signature" => request_signature} } 79 | 80 | it { expect(subject).to be_truthy } 81 | end 82 | 83 | context "when calculated signature does not match request signature" do 84 | 85 | let(:body) { {foo: "bar"}.to_json } 86 | let(:request_signature) { "going to fail" } 87 | let(:headers) { {"X-Hub-Signature" => request_signature} } 88 | 89 | it { expect(subject).to be_falsey } 90 | end 91 | end 92 | 93 | describe ".process_subscriptions" do 94 | 95 | context "without a callbacks block" do 96 | it "should raise an ArgumentError" do 97 | expect do 98 | @client.process_subscription(nil) 99 | end.to raise_error(ArgumentError) 100 | end 101 | end 102 | 103 | context "with a callbacks block and valid JSON" do 104 | 105 | before do 106 | @json = fixture("subscription_payload.json").read 107 | end 108 | 109 | it "should issue a callback to on_user_changed" do 110 | @client.process_subscription(@json) do |handler| 111 | handler.on_user_changed do |user_id, payload| 112 | expect(user_id).to eq("1234") 113 | end 114 | end 115 | end 116 | 117 | it "should issue a callback to on_tag_changed" do 118 | @client.process_subscription(@json) do |handler| 119 | handler.on_tag_changed do |tag_name, payload| 120 | expect(tag_name).to eq("nofilter") 121 | end 122 | end 123 | end 124 | 125 | it "should issue both callbacks in one block" do 126 | @client.process_subscription(@json) do |handler| 127 | 128 | handler.on_user_changed do |user_id, payload| 129 | expect(user_id).to eq("1234") 130 | end 131 | 132 | handler.on_tag_changed do |tag_name, payload| 133 | expect(tag_name).to eq("nofilter") 134 | end 135 | end 136 | end 137 | end 138 | end 139 | 140 | context "with a valid signature" do 141 | 142 | before do 143 | @json = fixture("subscription_payload.json").read 144 | end 145 | 146 | it "should not raise an Instagram::InvalidSignature error" do 147 | expect do 148 | @client.process_subscription(@json, :signature => "f1dbe2b6184ac2131209c87bba8e0382d089a8a2") do |handler| 149 | # hi 150 | end 151 | end.not_to raise_error 152 | end 153 | end 154 | 155 | context "with an invalid signature" do 156 | 157 | before do 158 | @json = fixture("subscription_payload.json").read 159 | end 160 | 161 | it "should raise an Instagram::InvalidSignature error" do 162 | invalid_signatures = ["31337H4X0R", nil] 163 | invalid_signatures.each do |signature| 164 | expect do 165 | @client.process_subscription(@json, :signature => signature ) do |handler| 166 | # hi 167 | end 168 | end.to raise_error(Instagram::InvalidSignature) 169 | end 170 | end 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /spec/instagram/api_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe Instagram::API do 4 | before do 5 | @keys = Instagram::Configuration::VALID_OPTIONS_KEYS 6 | end 7 | 8 | context "with module configuration" do 9 | 10 | before do 11 | Instagram.configure do |config| 12 | @keys.each do |key| 13 | config.send("#{key}=", key) 14 | end 15 | end 16 | end 17 | 18 | after do 19 | Instagram.reset 20 | end 21 | 22 | it "should inherit module configuration" do 23 | api = Instagram::API.new 24 | @keys.each do |key| 25 | expect(api.send(key)).to eq(key) 26 | end 27 | end 28 | 29 | context "with class configuration" do 30 | 31 | before do 32 | @configuration = { 33 | :access_token => 'AT', 34 | :adapter => :typhoeus, 35 | :client_id => 'CID', 36 | :client_secret => 'CS', 37 | :client_ips => '1.2.3.4', 38 | :connection_options => { :ssl => { :verify => true } }, 39 | :redirect_uri => 'http://http://localhost:4567/oauth/callback', 40 | :endpoint => 'http://tumblr.com/', 41 | :format => :xml, 42 | :proxy => 'http://shayne:sekret@proxy.example.com:8080', 43 | :scope => 'comments relationships', 44 | :user_agent => 'Custom User Agent', 45 | :no_response_wrapper => true, 46 | } 47 | end 48 | 49 | context "during initialization" 50 | 51 | it "should override module configuration" do 52 | api = Instagram::API.new(@configuration) 53 | @keys.each do |key| 54 | expect(api.send(key)).to eq(@configuration[key]) 55 | end 56 | end 57 | 58 | context "after initilization" do 59 | 60 | let(:api) { Instagram::API.new } 61 | 62 | before do 63 | @configuration.each do |key, value| 64 | api.send("#{key}=", value) 65 | end 66 | end 67 | 68 | it "should override module configuration after initialization" do 69 | @keys.each do |key| 70 | expect(api.send(key)).to eq(@configuration[key]) 71 | end 72 | end 73 | 74 | describe "#connection" do 75 | it "should use the connection_options" do 76 | expect(Faraday::Connection).to receive(:new).with(include(:ssl => { :verify => true })) 77 | api.send(:connection) 78 | end 79 | end 80 | end 81 | end 82 | end 83 | 84 | describe '#config' do 85 | subject { Instagram::API.new } 86 | 87 | let(:config) do 88 | c = {}; @keys.each {|key| c[key] = key }; c 89 | end 90 | 91 | it "returns a hash representing the configuration" do 92 | @keys.each do |key| 93 | subject.send("#{key}=", key) 94 | end 95 | expect(subject.config).to eq(config) 96 | end 97 | end 98 | 99 | describe ".authorize_url" do 100 | 101 | it "should generate an authorize URL with necessary params" do 102 | params = { :client_id => "CID", :client_secret => "CS" } 103 | 104 | client = Instagram::Client.new(params) 105 | 106 | redirect_uri = 'http://localhost:4567/oauth/callback' 107 | url = client.authorize_url(:redirect_uri => redirect_uri) 108 | 109 | options = { 110 | :redirect_uri => redirect_uri, 111 | :response_type => "code" 112 | } 113 | params2 = client.send(:authorization_params).merge(options) 114 | 115 | url2 = client.send(:connection).build_url("/oauth/authorize/", params2).to_s 116 | 117 | expect(url2).to eq(url) 118 | end 119 | 120 | it "should not include client secret in URL params" do 121 | params = { :client_id => "CID", :client_secret => "CS" } 122 | client = Instagram::Client.new(params) 123 | redirect_uri = 'http://localhost:4567/oauth/callback' 124 | url = client.authorize_url(:redirect_uri => redirect_uri) 125 | expect(url).not_to include("client_secret") 126 | end 127 | 128 | describe "scope param" do 129 | it "should include the scope if there is one set" do 130 | params = { :scope => "comments likes" } 131 | client = Instagram::Client.new(params) 132 | redirect_uri = 'http://localhost:4567/oauth/callback' 133 | url = client.authorize_url(:redirect_uri => redirect_uri) 134 | expect(url).to include("scope") 135 | end 136 | 137 | it "should not include the scope if the scope is blank" do 138 | params = { :scope => "" } 139 | client = Instagram::Client.new(params) 140 | redirect_uri = 'http://localhost:4567/oauth/callback' 141 | url = client.authorize_url(:redirect_uri => redirect_uri) 142 | expect(url).not_to include("scope") 143 | end 144 | end 145 | 146 | describe "redirect_uri" do 147 | it "should fall back to configuration redirect_uri if not passed as option" do 148 | redirect_uri = 'http://localhost:4567/oauth/callback' 149 | params = { :redirect_uri => redirect_uri } 150 | client = Instagram::Client.new(params) 151 | url = client.authorize_url() 152 | expect(url).to match(/redirect_uri=#{URI.escape(redirect_uri, Regexp.union('/',':'))}/) 153 | end 154 | 155 | it "should override configuration redirect_uri if passed as option" do 156 | redirect_uri_config = 'http://localhost:4567/oauth/callback_config' 157 | params = { :redirect_uri => redirect_uri_config } 158 | client = Instagram::Client.new(params) 159 | redirect_uri_option = 'http://localhost:4567/oauth/callback_option' 160 | options = { :redirect_uri => redirect_uri_option } 161 | url = client.authorize_url(options) 162 | expect(url).to match(/redirect_uri=#{URI.escape(redirect_uri_option, Regexp.union('/',':'))}/) 163 | end 164 | end 165 | end 166 | 167 | describe ".get_access_token" do 168 | 169 | describe "common functionality" do 170 | before do 171 | @client = Instagram::Client.new(:client_id => "CID", :client_secret => "CS") 172 | @url = @client.send(:connection).build_url("/oauth/access_token/").to_s 173 | stub_request(:post, @url). 174 | with(:body => {:client_id => "CID", :client_secret => "CS", :redirect_uri => "http://localhost:4567/oauth/callback", :grant_type => "authorization_code", :code => "C"}). 175 | to_return(:status => 200, :body => fixture("access_token.json"), :headers => {}) 176 | end 177 | 178 | it "should get the correct resource" do 179 | @client.get_access_token(code="C", :redirect_uri => "http://localhost:4567/oauth/callback") 180 | expect(a_request(:post, @url). 181 | with(:body => {:client_id => "CID", :client_secret => "CS", :redirect_uri => "http://localhost:4567/oauth/callback", :grant_type => "authorization_code", :code => "C"})). 182 | to have_been_made 183 | end 184 | 185 | it "should return a hash with an access_token and user data" do 186 | response = @client.get_access_token(code="C", :redirect_uri => "http://localhost:4567/oauth/callback") 187 | expect(response.access_token).to eq("at") 188 | expect(response.user.username).to eq("mikeyk") 189 | end 190 | end 191 | 192 | describe "redirect_uri param" do 193 | 194 | before do 195 | @redirect_uri_config = "http://localhost:4567/oauth/callback_config" 196 | @client = Instagram::Client.new(:client_id => "CID", :client_secret => "CS", :redirect_uri => @redirect_uri_config) 197 | @url = @client.send(:connection).build_url("/oauth/access_token/").to_s 198 | stub_request(:post, @url) 199 | end 200 | 201 | it "should fall back to configuration redirect_uri if not passed as option" do 202 | @client.get_access_token(code="C") 203 | expect(a_request(:post, @url). 204 | with(:body => hash_including({:redirect_uri => @redirect_uri_config}))). 205 | to have_been_made 206 | end 207 | 208 | it "should override configuration redirect_uri if passed as option" do 209 | redirect_uri_option = "http://localhost:4567/oauth/callback_option" 210 | @client.get_access_token(code="C", :redirect_uri => redirect_uri_option) 211 | expect(a_request(:post, @url). 212 | with(:body => hash_including({:redirect_uri => redirect_uri_option}))). 213 | to have_been_made 214 | end 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Instagram Ruby Gem 2 | ==================== 3 | A Ruby wrapper for the Instagram REST and Search APIs 4 | 5 | 6 | Installation 7 | ------------ 8 | gem install instagram 9 | 10 | Instagram REST and Search APIs 11 | ------------------------------ 12 | Our [developer site](http://instagram.com/developer) documents all the Instagram REST and Search APIs. 13 | 14 | 15 | Blog 16 | ---------------------------- 17 | The [Developer Blog] features news and important announcements about the Instagram Platform. You will also find tutorials and best practices to help you build great platform integrations. Make sure to subscribe to the RSS feed not to miss out on new posts: [http://developers.instagram.com](http://developers.instagram.com). 18 | 19 | 20 | Community 21 | ---------------------- 22 | The [Stack Overflow community](http://stackoverflow.com/questions/tagged/instagram/) is a great place to ask API related questions or if you need help with your code. Make sure to tag your questions with the Instagram tag to get fast answers from other fellow developers and members of the Instagram team. 23 | 24 | 25 | Does your project or organization use this gem? 26 | ----------------------------------------------- 27 | Add it to the [apps](http://github.com/Instagram/instagram-ruby-gem/wiki/apps) wiki! 28 | 29 | 30 | Sample Application 31 | ------------------ 32 | 33 | ```ruby 34 | require "sinatra" 35 | require "instagram" 36 | 37 | enable :sessions 38 | 39 | CALLBACK_URL = "http://localhost:4567/oauth/callback" 40 | 41 | Instagram.configure do |config| 42 | config.client_id = "YOUR_CLIENT_ID" 43 | config.client_secret = "YOUR_CLIENT_SECRET" 44 | # For secured endpoints only 45 | #config.client_ips = '' 46 | end 47 | 48 | get "/" do 49 | 'Connect with Instagram' 50 | end 51 | 52 | get "/oauth/connect" do 53 | redirect Instagram.authorize_url(:redirect_uri => CALLBACK_URL) 54 | end 55 | 56 | get "/oauth/callback" do 57 | response = Instagram.get_access_token(params[:code], :redirect_uri => CALLBACK_URL) 58 | session[:access_token] = response.access_token 59 | redirect "/nav" 60 | end 61 | 62 | get "/nav" do 63 | html = 64 | """ 65 |

Ruby Instagram Gem Sample Application

66 |
    67 |
  1. User Recent Media Calls user_recent_media - Get a list of a user's most recent media
  2. 68 |
  3. User Media Feed Calls user_media_feed - Get the currently authenticated user's media feed uses pagination
  4. 69 |
  5. Location Recent Media Calls location_recent_media - Get a list of recent media at a given location, in this case, the Instagram office
  6. 70 |
  7. Media Search Calls media_search - Get a list of media close to a given latitude and longitude
  8. 71 |
  9. Popular Media Calls media_popular - Get a list of the overall most popular media items
  10. 72 |
  11. User Search Calls user_search - Search for users on instagram, by name or username
  12. 73 |
  13. Location Search Calls location_search - Search for a location by lat/lng
  14. 74 |
  15. Location Search - 4Square Calls location_search - Search for a location by Fousquare ID (v2)
  16. 75 |
  17. TagsSearch for tags, view tag info and get media by tag
  18. 76 |
  19. View Rate Limit and Remaining API callsView remaining and ratelimit info.
  20. 77 |
78 | """ 79 | html 80 | end 81 | 82 | get "/user_recent_media" do 83 | client = Instagram.client(:access_token => session[:access_token]) 84 | user = client.user 85 | html = "

#{user.username}'s recent media

" 86 | for media_item in client.user_recent_media 87 | html << "

Like Un-Like
LikesCount=#{media_item.likes[:count]}
" 88 | end 89 | html 90 | end 91 | 92 | get '/media_like/:id' do 93 | client = Instagram.client(:access_token => session[:access_token]) 94 | client.like_media("#{params[:id]}") 95 | redirect "/user_recent_media" 96 | end 97 | 98 | get '/media_unlike/:id' do 99 | client = Instagram.client(:access_token => session[:access_token]) 100 | client.unlike_media("#{params[:id]}") 101 | redirect "/user_recent_media" 102 | end 103 | 104 | get "/user_media_feed" do 105 | client = Instagram.client(:access_token => session[:access_token]) 106 | user = client.user 107 | html = "

#{user.username}'s media feed

" 108 | 109 | page_1 = client.user_media_feed(777) 110 | page_2_max_id = page_1.pagination.next_max_id 111 | page_2 = client.user_recent_media(777, :max_id => page_2_max_id ) unless page_2_max_id.nil? 112 | html << "

Page 1


" 113 | for media_item in page_1 114 | html << "" 115 | end 116 | html << "

Page 2


" 117 | for media_item in page_2 118 | html << "" 119 | end 120 | html 121 | end 122 | 123 | get "/location_recent_media" do 124 | client = Instagram.client(:access_token => session[:access_token]) 125 | html = "

Media from the Instagram Office

" 126 | for media_item in client.location_recent_media(514276) 127 | html << "" 128 | end 129 | html 130 | end 131 | 132 | get "/media_search" do 133 | client = Instagram.client(:access_token => session[:access_token]) 134 | html = "

Get a list of media close to a given latitude and longitude

" 135 | for media_item in client.media_search("37.7808851","-122.3948632") 136 | html << "" 137 | end 138 | html 139 | end 140 | 141 | get "/media_popular" do 142 | client = Instagram.client(:access_token => session[:access_token]) 143 | html = "

Get a list of the overall most popular media items

" 144 | for media_item in client.media_popular 145 | html << "" 146 | end 147 | html 148 | end 149 | 150 | get "/user_search" do 151 | client = Instagram.client(:access_token => session[:access_token]) 152 | html = "

Search for users on instagram, by name or usernames

" 153 | for user in client.user_search("instagram") 154 | html << "
  • #{user.username} #{user.full_name}
  • " 155 | end 156 | html 157 | end 158 | 159 | get "/location_search" do 160 | client = Instagram.client(:access_token => session[:access_token]) 161 | html = "

    Search for a location by lat/lng with a radius of 5000m

    " 162 | for location in client.location_search("48.858844","2.294351","5000") 163 | html << "
  • #{location.name} Map
  • " 164 | end 165 | html 166 | end 167 | 168 | get "/location_search_4square" do 169 | client = Instagram.client(:access_token => session[:access_token]) 170 | html = "

    Search for a location by Fousquare ID (v2)

    " 171 | for location in client.location_search("3fd66200f964a520c5f11ee3") 172 | html << "
  • #{location.name} Map
  • " 173 | end 174 | html 175 | end 176 | 177 | get "/tags" do 178 | client = Instagram.client(:access_token => session[:access_token]) 179 | html = "

    Search for tags, get tag info and get media by tag

    " 180 | tags = client.tag_search('cat') 181 | html << "

    Tag Name = #{tags[0].name}. Media Count = #{tags[0].media_count}.



    " 182 | for media_item in client.tag_recent_media(tags[0].name) 183 | html << "" 184 | end 185 | html 186 | end 187 | 188 | get "/limits" do 189 | client = Instagram.client(:access_token => session[:access_token]) 190 | html = "

    View API Rate Limit and calls remaining

    " 191 | response = client.utils_raw_response 192 | html << "Rate Limit = #{response.headers[:x_ratelimit_limit]}.
    Calls Remaining = #{response.headers[:x_ratelimit_remaining]}" 193 | 194 | html 195 | end 196 | ``` 197 | 198 | Contributing 199 | ------------ 200 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. 201 | 202 | Here are some ways *you* can contribute: 203 | 204 | * by using alpha, beta, and prerelease versions 205 | * by reporting bugs 206 | * by suggesting new features 207 | * by writing or editing documentation 208 | * by writing specifications 209 | * by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) 210 | * by refactoring code 211 | * by closing [issues](http://github.com/Instagram/instagram-ruby-gem/issues) 212 | * by reviewing patches 213 | 214 | 215 | Submitting an Issue 216 | ------------------- 217 | We use the [GitHub issue tracker](http://github.com/Instagram/instagram-ruby-gem/issues) to track bugs and 218 | features. Before submitting a bug report or feature request, check to make sure it hasn't already 219 | been submitted. You can indicate support for an existing issue by voting it up. When submitting a 220 | bug report, please include a [Gist](http://gist.github.com/) that includes a stack trace and any 221 | details that may be necessary to reproduce the bug, including your gem version, Ruby version, and 222 | operating system. Ideally, a bug report should include a pull request with failing specs. 223 | 224 | Instagram has a [bounty program](https://www.facebook.com/whitehat/) for the safe 225 | disclosure of security bugs. In those cases, please go through the process 226 | outlined on that page and do not file a public issue. 227 | 228 | 229 | Submitting a Pull Request 230 | ------------------------- 231 | 1. Fork the project. 232 | 2. Create a topic branch. 233 | 3. Implement your feature or bug fix. 234 | 4. Add documentation for your feature or bug fix. 235 | 5. Run rake doc:yard. If your changes are not 100% documented, go back to step 4. 236 | 6. Add specs for your feature or bug fix. 237 | 7. Run rake spec. If your changes are not 100% covered, go back to step 6. 238 | 8. Commit and push your changes. 239 | 9. Submit a pull request. Please do not include changes to the gemspec, version, or history file. (If you want to create your own version for some reason, please do so in a separate commit.) 240 | 10. If you haven't already, complete the Contributor License Agreement ("CLA"). 241 | 242 | Contributor License Agreement ("CLA") 243 | _____________________________________ 244 | In order to accept your pull request, we need you to submit a CLA. You only need 245 | to do this once to work on any of Instagram's or Facebook's open source projects. 246 | 247 | Complete your CLA here: [https://code.facebook.com/cla](https://code.facebook.com/cla) 248 | 249 | 250 | Copyright 251 | --------- 252 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 253 | By contributing to Instgram Ruby Gem, you agree that your contributions will be licensed under its BSD license. 254 | See [LICENSE](https://github.com/Instagram/instagram-ruby-gem/blob/master/LICENSE.md) for details. 255 | -------------------------------------------------------------------------------- /lib/instagram/client/subscriptions.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'multi_json' 3 | 4 | module Instagram 5 | class Client 6 | # Defines methods related to real-time 7 | module Subscriptions 8 | # Returns a list of active real-time subscriptions 9 | # 10 | # @overload subscriptions(options={}) 11 | # @return [Hashie::Mash] The list of subscriptions. 12 | # @example Returns a list of subscriptions for the authenticated application 13 | # Instagram.subscriptions 14 | # @format :json 15 | # @authenticated true 16 | # 17 | # Requires client_secret to be set on the client or passed in options 18 | # @rate_limited true 19 | # @see https://api.instagram.com/developer/realtime/ 20 | def subscriptions(options={}) 21 | response = get("subscriptions", options.merge(:client_secret => client_secret)) 22 | response 23 | end 24 | 25 | # Creates a real-time subscription 26 | # 27 | # @overload create_subscription(options={}) 28 | # @param options [Hash] A set of parameters 29 | # @option options [String] :object The object you'd like to subscribe to (user, tag, location or geography) 30 | # @option options [String] :callback_url The subscription callback URL 31 | # @option options [String] :aspect The aspect of the object you'd like to subscribe to (in this case, "media"). 32 | # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively 33 | # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object 34 | # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object 35 | # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point 36 | # @overload create_subscription(object, callback_url, aspect="media", options={}) 37 | # @param object [String] The object you'd like to subscribe to (user, tag, location or geography) 38 | # @param callback_url [String] The subscription callback URL 39 | # @param aspect [String] he aspect of the object you'd like to subscribe to (in this case, "media"). 40 | # @param options [Hash] Addition options and parameters 41 | # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively 42 | # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object 43 | # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object 44 | # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point 45 | # 46 | # Note that we only support "media" at this time, but we might support other types of subscriptions in the future. 47 | # @return [Hashie::Mash] The subscription created. 48 | # @example Creates a new subscription to receive notifications for user media changes. 49 | # Instagram.create_subscription("user", "http://example.com/instagram/callback") 50 | # @format :json 51 | # @authenticated true 52 | # 53 | # Requires client_secret to be set on the client or passed in options 54 | # @rate_limited true 55 | # @see https://api.instagram.com/developer/realtime/ 56 | def create_subscription(*args) 57 | options = args.last.is_a?(Hash) ? args.pop : {} 58 | object = args.shift 59 | callback_url = args.shift 60 | aspect = args.shift 61 | options.tap {|o| 62 | o[:object] = object unless object.nil? 63 | o[:callback_url] = callback_url unless callback_url.nil? 64 | o[:aspect] = aspect || o[:aspect] || "media" 65 | } 66 | response = post("subscriptions", options.merge(:client_secret => client_secret)) 67 | response 68 | end 69 | 70 | # Deletes a real-time subscription 71 | # 72 | # @overload delete_subscription(options={}) 73 | # @param options [Hash] Addition options and parameters 74 | # @option options [Integer] :subscription_id The subscription's ID 75 | # @option options [String] :object When specified will remove all subscriptions of this object type, unless an :object_id is also specified (user, tag, location or geography) 76 | # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id 77 | # @overload delete_subscription(subscription_id, options={}) 78 | # @param subscription_id [Integer] The subscription's ID 79 | # @param options [Hash] Addition options and parameters 80 | # @option options [String] :object When specified will remove all subscriptions of this object type, unless an :object_id is also specified (user, tag, location or geography) 81 | # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id 82 | # @return [Hashie::Mash] 83 | # @example Deletes an application's user change subscription 84 | # Instagram.delete_subscription(:object => "user") 85 | # @format :json 86 | # @authenticated true 87 | # 88 | # Requires client_secret to be set on the client or passed in options 89 | # @rate_limited true 90 | # @see https://api.instagram.com/developer/realtime/ 91 | def delete_subscription(*args) 92 | options = args.last.is_a?(Hash) ? args.pop : {} 93 | subscription_id = args.first 94 | options.merge!(:id => subscription_id) if subscription_id 95 | response = delete("subscriptions", options.merge(:client_secret => client_secret)) 96 | response 97 | end 98 | 99 | # As a security measure (to prevent DDoS attacks), Instagram sends a verification request to your server 100 | # after you request a subscription. 101 | # This method parses the challenge params and makes sure the call is legitimate. 102 | # 103 | # @param params the request parameters sent by Instagram. (You can pass in a Rails params hash.) 104 | # @param verify_token the verify token sent in the {#subscribe subscription request}, if you provided one 105 | # 106 | # @yield verify_token if you need to compute the verification token 107 | # (for instance, if your callback URL includes a record ID, which you look up 108 | # and use to calculate a hash), you can pass meet_challenge a block, which 109 | # will receive the verify_token received back from Instagram. 110 | # 111 | # @return the challenge string to be sent back to Instagram, or false if the request is invalid. 112 | def meet_challenge(params, verify_token = nil, &verification_block) 113 | if params["hub.mode"] == "subscribe" && 114 | # you can make sure this is legitimate through two ways 115 | # if your store the token across the calls, you can pass in the token value 116 | # and we'll make sure it matches 117 | ((verify_token && params["hub.verify_token"] == verify_token) || 118 | # alternately, if you sent a specially-constructed value (such as a hash of various secret values) 119 | # you can pass in a block, which we'll call with the verify_token sent by Instagram 120 | # if it's legit, return anything that evaluates to true; otherwise, return nil or false 121 | (verification_block && yield(params["hub.verify_token"]))) 122 | params["hub.challenge"] 123 | else 124 | false 125 | end 126 | end 127 | 128 | # Public: As a security measure, all updates from Instagram are signed using 129 | # X-Hub-Signature: XXXX where XXX is the sha1 of the json payload 130 | # using your application secret as the key. 131 | # 132 | # Example: 133 | # # in Rails controller 134 | # def receive_update 135 | # if Instagram.validate_update(request.body, headers) 136 | # ... 137 | # else 138 | # render text: "not authorized", status: 401 139 | # end 140 | # end 141 | def validate_update(body, headers) 142 | unless client_secret 143 | raise ArgumentError, "client_secret must be set during configure" 144 | end 145 | 146 | if request_signature = headers['X-Hub-Signature'] || headers['HTTP_X_HUB_SIGNATURE'] 147 | calculated_signature = OpenSSL::HMAC.hexdigest('sha1', client_secret, body) 148 | calculated_signature == request_signature 149 | end 150 | end 151 | 152 | # Process a subscription notification JSON payload 153 | # 154 | # @overload process_subscription(json, &block) 155 | # @param json [String] The JSON response received by the Instagram real-time server 156 | # @param block [Proc] A callable in which callbacks are defined 157 | # @option options [String] :signature Pass in an X-Hub-Signature to use for payload validation 158 | # @return [nil] 159 | # @example Process and handle a notification for a user media change 160 | # Instagram.process_subscription(params[:body]) do |handler| 161 | # 162 | # handler.on_user_changed do |user_id, data| 163 | # 164 | # user = User.by_instagram_id(user_id) 165 | # @client = Instagram.client(:access_token => _access_token_for_user(user)) 166 | # latest_media = @client.user_recent_media[0] 167 | # user.media.create_with_hash(latest_media) 168 | # end 169 | # 170 | # end 171 | # @format :json 172 | # @authenticated true 173 | # 174 | # Requires client_secret to be set on the client or passed in options 175 | # @rate_limited true 176 | # @see https://api.instagram.com/developer/realtime/ 177 | def process_subscription(json, options={}, &block) 178 | raise ArgumentError, "callbacks block expected" unless block_given? 179 | 180 | if options.has_key?(:signature) 181 | if !client_secret 182 | raise ArgumentError, "client_secret must be set during configure" 183 | end 184 | digest = OpenSSL::Digest.new('sha1') 185 | verify_signature = OpenSSL::HMAC.hexdigest(digest, client_secret, json) 186 | 187 | if options[:signature] != verify_signature 188 | raise Instagram::InvalidSignature, "invalid X-Hub-Signature does not match verify signature against client_secret" 189 | end 190 | end 191 | 192 | payload = MultiJson.decode(json) 193 | @changes = Hash.new { |h,k| h[k] = [] } 194 | for change in payload 195 | @changes[change['object']] << change 196 | end 197 | block.call(self) 198 | end 199 | 200 | [:user, :tag, :location, :geography].each do |object| 201 | class_eval <<-RUBY_EVAL, __FILE__, __LINE__ +1 202 | def on_#{object}_changed(&block) 203 | for change in @changes['#{object}'] 204 | yield change.delete('object_id'), change 205 | end 206 | end 207 | RUBY_EVAL 208 | end 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/instagram/client/users.rb: -------------------------------------------------------------------------------- 1 | module Instagram 2 | class Client 3 | # Defines methods related to users 4 | module Users 5 | # Returns extended information of a given user 6 | # 7 | # @overload user(id=nil, options={}) 8 | # @param user [Integer] An Instagram user ID 9 | # @return [Hashie::Mash] The requested user. 10 | # @example Return extended information for @shayne 11 | # Instagram.user(20) 12 | # @format :json 13 | # @authenticated false unless requesting it from a protected user 14 | # 15 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 16 | # @rate_limited true 17 | # @see http://instagram.com/developer/endpoints/users/#get_users 18 | def user(*args) 19 | options = args.last.is_a?(Hash) ? args.pop : {} 20 | id = args.first || 'self' 21 | response = get("users/#{id}", options) 22 | response 23 | end 24 | 25 | # Returns users that match the given query 26 | # 27 | # @format :json 28 | # @authenticated false 29 | # @rate_limited true 30 | # @param query [String] The search query to run against user search. 31 | # @param options [Hash] A customizable set of options. 32 | # @option options [Integer] :count The number of users to retrieve. 33 | # @return [Hashie::Mash] 34 | # @see http://instagram.com/developer/endpoints/users/#get_users_search 35 | # @example Return users that match "Shayne Sweeney" 36 | # Instagram.user_search("Shayne Sweeney") 37 | def user_search(query, options={}) 38 | response = get('users/search', options.merge(:q => query)) 39 | response 40 | end 41 | 42 | # Returns a list of users whom a given user follows 43 | # 44 | # @overload user_follows(id=nil, options={}) 45 | # @param options [Hash] A customizable set of options. 46 | # @return [Hashie::Mash] 47 | # @example Returns a list of users the authenticated user follows 48 | # Instagram.user_follows 49 | # @overload user_follows(id=nil, options={}) 50 | # @param user [Integer] An Instagram user ID. 51 | # @param options [Hash] A customizable set of options. 52 | # @option options [Integer] :cursor (nil) Breaks the results into pages. Provide values as returned in the response objects's next_cursor attribute to page forward in the list. 53 | # @option options [Integer] :count (nil) Limits the number of results returned per page. 54 | # @return [Hashie::Mash] 55 | # @example Return a list of users @mikeyk follows 56 | # Instagram.user_follows(4) # @mikeyk user ID being 4 57 | # @see http://instagram.com/developer/endpoints/relationships/#get_users_follows 58 | # @format :json 59 | # @authenticated false unless requesting it from a protected user 60 | # 61 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 62 | # @rate_limited true 63 | def user_follows(*args) 64 | options = args.last.is_a?(Hash) ? args.pop : {} 65 | id = args.first || "self" 66 | response = get("users/#{id}/follows", options) 67 | response 68 | end 69 | end 70 | 71 | # Returns a list of users whom a given user is followed by 72 | # 73 | # @overload user_followed_by(id=nil, options={}) 74 | # @param options [Hash] A customizable set of options. 75 | # @return [Hashie::Mash] 76 | # @example Returns a list of users the authenticated user is followed by 77 | # Instagram.user_followed_by 78 | # @overload user_followed_by(id=nil, options={}) 79 | # @param user [Integer] An Instagram user ID. 80 | # @param options [Hash] A customizable set of options. 81 | # @option options [Integer] :cursor (nil) Breaks the results into pages. Provide values as returned in the response objects's next_cursor attribute to page forward in the list. 82 | # @option options [Integer] :count (nil) Limits the number of results returned per page. 83 | # @return [Hashie::Mash] 84 | # @example Return a list of users @mikeyk is followed by 85 | # Instagram.user_followed_by(4) # @mikeyk user ID being 4 86 | # @see http://instagram.com/developer/endpoints/relationships/#get_users_followed_by 87 | # @format :json 88 | # @authenticated false unless requesting it from a protected user 89 | # 90 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 91 | # @rate_limited true 92 | def user_followed_by(*args) 93 | options = args.last.is_a?(Hash) ? args.pop : {} 94 | id = args.first || "self" 95 | response = get("users/#{id}/followed-by", options) 96 | response 97 | end 98 | 99 | # Returns a list of users who have requested the currently authorized user's permission to follow 100 | # 101 | # @overload user_requested_by() 102 | # @param options [Hash] A customizable set of options. 103 | # @return [Hashie::Mash] 104 | # @example Returns a list of users awaiting approval of a ollow request, for the authenticated user 105 | # Instagram.user_requested_by 106 | # @overload user_requested_by() 107 | # @return [Hashie::Mash] 108 | # @example Return a list of users who have requested to follow the authenticated user 109 | # Instagram.user_requested_by() 110 | # @see http://instagram.com/developer/endpoints/relationships/#get_incoming_requests 111 | # @format :json 112 | # @authenticated true 113 | # @rate_limited true 114 | def user_requested_by() 115 | response = get("users/self/requested-by") 116 | response 117 | end 118 | 119 | # Returns most recent media items from the currently authorized user's feed 120 | # 121 | # @overload user_media_feed(options={}) 122 | # @param options [Hash] A customizable set of options. 123 | # @option options [Integer] :max_id Returns results with an ID less than (that is, older than) or equal to the specified ID. 124 | # @option options [Integer] :min_id Return media later than this min_id 125 | # @option options [Integer] :count Specifies the number of records to retrieve, per page. 126 | # @return [Hashie::Mash] 127 | # @example Return most recent media images that would appear on @shayne's feed 128 | # Instagram.user_media_feed() # assuming @shayne is the authorized user 129 | # @format :json 130 | # @authenticated true 131 | # @rate_limited true 132 | # @see http://instagram.com/developer/endpoints/users/#get_users_feed 133 | def user_media_feed(*args) 134 | options = args.first.is_a?(Hash) ? args.pop : {} 135 | response = get('users/self/feed', options) 136 | response 137 | end 138 | 139 | # Returns a list of recent media items for a given user 140 | # 141 | # @overload user_recent_media(options={}) 142 | # @param options [Hash] A customizable set of options. 143 | # @return [Hashie::Mash] 144 | # @example Returns a list of recent media items for the currently authenticated user 145 | # Instagram.user_recent_media 146 | # @overload user_recent_media(id=nil, options={}) 147 | # @param user [Integer] An Instagram user ID. 148 | # @param options [Hash] A customizable set of options. 149 | # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. 150 | # @option options [Integer] :count (nil) Limits the number of results returned per page. 151 | # @return [Hashie::Mash] 152 | # @example Return a list of media items taken by @mikeyk 153 | # Instagram.user_recent_media(4) # @mikeyk user ID being 4 154 | # @see http://instagram.com/developer/endpoints/users/#get_users_media_recent 155 | # @format :json 156 | # @authenticated false unless requesting it from a protected user 157 | # 158 | # If getting this data of a protected user, you must authenticate (and be allowed to see that user). 159 | # @rate_limited true 160 | def user_recent_media(*args) 161 | options = args.last.is_a?(Hash) ? args.pop : {} 162 | id = args.first || "self" 163 | response = get("users/#{id}/media/recent", options) 164 | response 165 | end 166 | 167 | # Returns a list of media items liked by the current user 168 | # 169 | # @overload user_liked_media(options={}) 170 | # @param options [Hash] A customizable set of options. 171 | # @option options [Integer] :max_like_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. 172 | # @option options [Integer] :count (nil) Limits the number of results returned per page. 173 | # @return [Hashie::Mash] 174 | # @example Returns a list of media items liked by the currently authenticated user 175 | # Instagram.user_liked_media 176 | # @see http://instagram.com/developer/endpoints/users/#get_users_liked_feed 177 | # @format :json 178 | # @authenticated true 179 | # @rate_limited true 180 | def user_liked_media(options={}) 181 | response = get("users/self/media/liked", options) 182 | response 183 | end 184 | 185 | # Returns information about the current user's relationship (follow/following/etc) to another user 186 | # 187 | # @overload user_relationship(id, options={}) 188 | # @param user [Integer] An Instagram user ID. 189 | # @param options [Hash] An optional options hash 190 | # @return [Hashie::Mash] 191 | # @example Return the relationship status between the currently authenticated user and @mikeyk 192 | # Instagram.user_relationship(4) # @mikeyk user ID being 4 193 | # @see http://instagram.com/developer/endpoints/relationships/#get_relationship 194 | # @format :json 195 | # @authenticated true 196 | # @rate_limited true 197 | def user_relationship(id, options={}) 198 | response = get("users/#{id}/relationship", options) 199 | response 200 | end 201 | 202 | # Create a follows relationship between the current user and the target user 203 | # 204 | # @overload follow_user(id, options={}) 205 | # @param user [Integer] An Instagram user ID. 206 | # @param options [Hash] An optional options hash 207 | # @return [Hashie::Mash] 208 | # @example Request the current user to follow the target user 209 | # Instagram.follow_user(4) 210 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 211 | # @format :json 212 | # @authenticated true 213 | # @rate_limited true 214 | def follow_user(id, options={}) 215 | options["action"] = "follow" 216 | response = post("users/#{id}/relationship", options, signature=true) 217 | response 218 | end 219 | 220 | # Destroy a follows relationship between the current user and the target user 221 | # 222 | # @overload unfollow_user(id, options={}) 223 | # @param user [Integer] An Instagram user ID. 224 | # @param options [Hash] An optional options hash 225 | # @return [Hashie::Mash] 226 | # @example Remove a follows relationship between the current user and the target user 227 | # Instagram.unfollow_user(4) 228 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 229 | # @format :json 230 | # @authenticated true 231 | # @rate_limited true 232 | def unfollow_user(id, options={}) 233 | options["action"] = "unfollow" 234 | response = post("users/#{id}/relationship", options, signature=true) 235 | response 236 | end 237 | 238 | # Block a relationship between the current user and the target user 239 | # 240 | # @overload unfollow_user(id, options={}) 241 | # @param user [Integer] An Instagram user ID. 242 | # @param options [Hash] An optional options hash 243 | # @return [Hashie::Mash] 244 | # @example Block a relationship between the current user and the target user 245 | # Instagram.block_user(4) 246 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 247 | # @format :json 248 | # @authenticated true 249 | # @rate_limited true 250 | def block_user(id, options={}) 251 | options["action"] = "block" 252 | response = post("users/#{id}/relationship", options, signature=true) 253 | response 254 | end 255 | 256 | # Remove a relationship block between the current user and the target user 257 | # 258 | # @overload unblock_user(id, options={}) 259 | # @param user [Integer] An Instagram user ID. 260 | # @param options [Hash] An optional options hash 261 | # @return [Hashie::Mash] 262 | # @example Remove a relationship block between the current user and the target user 263 | # Instagram.unblock_user(4) 264 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 265 | # @format :json 266 | # @authenticated true 267 | # @rate_limited true 268 | def unblock_user(id, options={}) 269 | options["action"] = "unblock" 270 | response = post("users/#{id}/relationship", options, signature=true) 271 | response 272 | end 273 | 274 | # Approve a relationship request between the current user and the target user 275 | # 276 | # @overload approve_user(id, options={}) 277 | # @param user [Integer] An Instagram user ID. 278 | # @param options [Hash] An optional options hash 279 | # @return [Hashie::Mash] 280 | # @example Approve a relationship request between the current user and the target user 281 | # Instagram.approve_user(4) 282 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 283 | # @format :json 284 | # @authenticated true 285 | # @rate_limited true 286 | def approve_user(id, options={}) 287 | options["action"] = "approve" 288 | response = post("users/#{id}/relationship", options, signature=true) 289 | response 290 | end 291 | 292 | # Deny a relationship request between the current user and the target user 293 | # 294 | # @overload deny_user(id, options={}) 295 | # @param user [Integer] An Instagram user ID. 296 | # @param options [Hash] An optional options hash 297 | # @return [Hashie::Mash] 298 | # @example Deny a relationship request between the current user and the target user 299 | # Instagram.deny_user(4) 300 | # @see http://instagram.com/developer/endpoints/relationships/#post_relationship 301 | # @format :json 302 | # @authenticated true 303 | # @rate_limited true 304 | def deny_user(id, options={}) 305 | options["action"] = "deny" 306 | response = post("users/#{id}/relationship", options, signature=true) 307 | response 308 | end 309 | end 310 | end 311 | -------------------------------------------------------------------------------- /spec/instagram/client/users_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../spec_helper', __FILE__) 2 | 3 | describe Instagram::Client do 4 | Instagram::Configuration::VALID_FORMATS.each do |format| 5 | context ".new(:format => '#{format}')" do 6 | before do 7 | @client = Instagram::Client.new(:format => format, :client_id => 'CID', :client_secret => 'CS', :access_token => 'AT') 8 | end 9 | 10 | describe ".user" do 11 | 12 | context "with user ID passed" do 13 | 14 | before do 15 | stub_get("users/4.#{format}"). 16 | with(:query => {:access_token => @client.access_token}). 17 | to_return(:body => fixture("mikeyk.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 18 | end 19 | 20 | it "should get the correct resource" do 21 | @client.user(4) 22 | expect(a_get("users/4.#{format}"). 23 | with(:query => {:access_token => @client.access_token})). 24 | to have_been_made 25 | end 26 | 27 | it "should return extended information of a given user" do 28 | user = @client.user(4) 29 | expect(user.full_name).to eq("Mike Krieger") 30 | end 31 | 32 | end 33 | 34 | context "without user ID passed" do 35 | 36 | before do 37 | stub_get("users/self.#{format}"). 38 | with(:query => {:access_token => @client.access_token}). 39 | to_return(:body => fixture("shayne.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 40 | end 41 | 42 | it "should get the correct resource" do 43 | @client.user() 44 | expect(a_get("users/self.#{format}"). 45 | with(:query => {:access_token => @client.access_token})). 46 | to have_been_made 47 | end 48 | end 49 | end 50 | 51 | describe ".user_search" do 52 | 53 | before do 54 | stub_get("users/search.#{format}"). 55 | with(:query => {:access_token => @client.access_token}). 56 | with(:query => {:q => "Shayne Sweeney"}). 57 | to_return(:body => fixture("user_search.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 58 | end 59 | 60 | it "should get the correct resource" do 61 | @client.user_search("Shayne Sweeney") 62 | expect(a_get("users/search.#{format}"). 63 | with(:query => {:access_token => @client.access_token}). 64 | with(:query => {:q => "Shayne Sweeney"})). 65 | to have_been_made 66 | end 67 | 68 | it "should return an array of user search results" do 69 | users = @client.user_search("Shayne Sweeney") 70 | expect(users).to be_a Array 71 | expect(users.first.username).to eq("shayne") 72 | end 73 | end 74 | 75 | describe ".user_follows" do 76 | 77 | context "with user ID passed" do 78 | 79 | before do 80 | stub_get("users/4/follows.#{format}"). 81 | with(:query => {:access_token => @client.access_token}). 82 | to_return(:body => fixture("follows.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 83 | end 84 | 85 | it "should get the correct resource" do 86 | @client.user_follows(4) 87 | expect(a_get("users/4/follows.#{format}"). 88 | with(:query => {:access_token => @client.access_token})). 89 | to have_been_made 90 | end 91 | 92 | it "should return a list of users whom a given user follows" do 93 | follows = @client.user_follows(4) 94 | expect(follows).to be_a Array 95 | expect(follows.first.username).to eq("heartsf") 96 | end 97 | end 98 | 99 | context "without user ID passed" do 100 | 101 | before do 102 | stub_get("users/self/follows.#{format}"). 103 | with(:query => {:access_token => @client.access_token}). 104 | to_return(:body => fixture("follows.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 105 | end 106 | 107 | it "should get the correct resource" do 108 | @client.user_follows 109 | expect(a_get("users/self/follows.#{format}"). 110 | with(:query => {:access_token => @client.access_token})). 111 | to have_been_made 112 | end 113 | end 114 | end 115 | 116 | describe ".user_followed_by" do 117 | 118 | context "with user ID passed" do 119 | 120 | before do 121 | stub_get("users/4/followed-by.#{format}"). 122 | with(:query => {:access_token => @client.access_token}). 123 | to_return(:body => fixture("followed_by.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 124 | end 125 | 126 | it "should get the correct resource" do 127 | @client.user_followed_by(4) 128 | expect(a_get("users/4/followed-by.#{format}"). 129 | with(:query => {:access_token => @client.access_token})). 130 | to have_been_made 131 | end 132 | 133 | it "should return a list of users whom a given user is followed by" do 134 | followed_by = @client.user_followed_by(4) 135 | expect(followed_by).to be_a Array 136 | expect(followed_by.first.username).to eq("bojieyang") 137 | end 138 | end 139 | 140 | context "without user ID passed" do 141 | 142 | before do 143 | stub_get("users/self/followed-by.#{format}"). 144 | with(:query => {:access_token => @client.access_token}). 145 | to_return(:body => fixture("followed_by.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 146 | end 147 | 148 | it "should get the correct resource" do 149 | @client.user_followed_by 150 | expect(a_get("users/self/followed-by.#{format}"). 151 | with(:query => {:access_token => @client.access_token})). 152 | to have_been_made 153 | end 154 | end 155 | end 156 | 157 | describe ".user_media_feed" do 158 | 159 | before do 160 | stub_get("users/self/feed.#{format}"). 161 | with(:query => {:access_token => @client.access_token}). 162 | to_return(:body => fixture("user_media_feed.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 163 | end 164 | 165 | it "should get the correct resource" do 166 | @client.user_media_feed 167 | expect(a_get("users/self/feed.#{format}"). 168 | with(:query => {:access_token => @client.access_token})). 169 | to have_been_made 170 | end 171 | 172 | context Instagram::Response do 173 | let(:user_media_feed_response){ @client.user_media_feed } 174 | subject{ user_media_feed_response } 175 | 176 | it{ is_expected.to be_an_instance_of(Array) } 177 | it{ is_expected.to be_a_kind_of(Instagram::Response) } 178 | it{ is_expected.to respond_to(:pagination) } 179 | it{ is_expected.to respond_to(:meta) } 180 | 181 | context '.pagination' do 182 | subject{ user_media_feed_response.pagination } 183 | 184 | it{ is_expected.to be_an_instance_of(Hashie::Mash) } 185 | 186 | describe '#next_max_id' do 187 | subject { super().next_max_id } 188 | it { is_expected.to eq('22063131') } 189 | end 190 | end 191 | 192 | context '.meta' do 193 | subject{ user_media_feed_response.meta } 194 | 195 | it{ is_expected.to be_an_instance_of(Hashie::Mash) } 196 | 197 | describe '#code' do 198 | subject { super().code } 199 | it { is_expected.to eq(200) } 200 | end 201 | end 202 | end 203 | end 204 | 205 | describe ".user_liked_media" do 206 | 207 | before do 208 | stub_get("users/self/media/liked.#{format}"). 209 | with(:query => {:access_token => @client.access_token}). 210 | to_return(:body => fixture("liked_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 211 | end 212 | 213 | it "should get the correct resource" do 214 | @client.user_liked_media 215 | expect(a_get("users/self/media/liked.#{format}"). 216 | with(:query => {:access_token => @client.access_token})). 217 | to have_been_made 218 | end 219 | end 220 | 221 | describe ".user_recent_media" do 222 | 223 | context "with user ID passed" do 224 | 225 | before do 226 | stub_get("users/4/media/recent.#{format}"). 227 | with(:query => {:access_token => @client.access_token}). 228 | to_return(:body => fixture("recent_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 229 | end 230 | 231 | it "should get the correct resource" do 232 | @client.user_recent_media(4) 233 | expect(a_get("users/4/media/recent.#{format}"). 234 | with(:query => {:access_token => @client.access_token})). 235 | to have_been_made 236 | end 237 | 238 | it "should return a list of recent media items for the given user" do 239 | recent_media = @client.user_recent_media(4) 240 | expect(recent_media).to be_a Array 241 | expect(recent_media.first.user.username).to eq("shayne") 242 | end 243 | end 244 | 245 | context "without user ID passed" do 246 | 247 | before do 248 | stub_get("users/self/media/recent.#{format}"). 249 | with(:query => {:access_token => @client.access_token}). 250 | to_return(:body => fixture("recent_media.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 251 | end 252 | 253 | it "should get the correct resource" do 254 | @client.user_recent_media 255 | expect(a_get("users/self/media/recent.#{format}"). 256 | with(:query => {:access_token => @client.access_token})). 257 | to have_been_made 258 | end 259 | end 260 | end 261 | 262 | describe ".user_requested_by" do 263 | 264 | before do 265 | stub_get("users/self/requested-by.#{format}"). 266 | with(:query => {:access_token => @client.access_token}). 267 | to_return(:body => fixture("requested_by.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 268 | end 269 | 270 | it "should get the correct resource" do 271 | @client.user_requested_by 272 | expect(a_get("users/self/requested-by.#{format}"). 273 | with(:query => {:access_token => @client.access_token})). 274 | to have_been_made 275 | end 276 | 277 | it "should return a list of users awaiting approval" do 278 | users = @client.user_requested_by 279 | expect(users).to be_a Array 280 | expect(users.first.username).to eq("shayne") 281 | end 282 | end 283 | 284 | describe ".user_relationship" do 285 | 286 | before do 287 | stub_get("users/4/relationship.#{format}"). 288 | with(:query => {:access_token => @client.access_token}). 289 | to_return(:body => fixture("relationship.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 290 | end 291 | 292 | it "should get the correct resource" do 293 | @client.user_relationship(4) 294 | expect(a_get("users/4/relationship.#{format}"). 295 | with(:query => {:access_token => @client.access_token})). 296 | to have_been_made 297 | end 298 | 299 | it "should return a relationship status response" do 300 | status = @client.user_relationship(4) 301 | expect(status.incoming_status).to eq("requested_by") 302 | end 303 | end 304 | 305 | describe ".follow_user" do 306 | 307 | before do 308 | stub_post("users/4/relationship.#{format}"). 309 | with(:body => {:action => "follow", :access_token => @client.access_token}). 310 | to_return(:body => fixture("follow_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 311 | end 312 | 313 | it "should get the correct resource" do 314 | @client.follow_user(4) 315 | expect(a_post("users/4/relationship.#{format}"). 316 | with(:body => {:action => "follow", :access_token => @client.access_token})). 317 | to have_been_made 318 | end 319 | 320 | it "should return a relationship status response" do 321 | status = @client.follow_user(4) 322 | expect(status.outgoing_status).to eq("requested") 323 | end 324 | end 325 | 326 | describe ".unfollow_user" do 327 | 328 | before do 329 | stub_post("users/4/relationship.#{format}"). 330 | with(:body => {:action => "unfollow", :access_token => @client.access_token}). 331 | to_return(:body => fixture("unfollow_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 332 | end 333 | 334 | it "should get the correct resource" do 335 | @client.unfollow_user(4) 336 | expect(a_post("users/4/relationship.#{format}"). 337 | with(:body => {:action => "unfollow", :access_token => @client.access_token})). 338 | to have_been_made 339 | end 340 | 341 | it "should return a relationship status response" do 342 | status = @client.unfollow_user(4) 343 | expect(status.outgoing_status).to eq("none") 344 | end 345 | end 346 | 347 | describe ".block_user" do 348 | 349 | before do 350 | stub_post("users/4/relationship.#{format}"). 351 | with(:body => {:action => "block", :access_token => @client.access_token}). 352 | to_return(:body => fixture("block_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 353 | end 354 | 355 | it "should get the correct resource" do 356 | @client.block_user(4) 357 | expect(a_post("users/4/relationship.#{format}"). 358 | with(:body => {:action => "block", :access_token => @client.access_token})). 359 | to have_been_made 360 | end 361 | 362 | it "should return a relationship status response" do 363 | status = @client.block_user(4) 364 | expect(status.outgoing_status).to eq("none") 365 | end 366 | end 367 | 368 | describe ".unblock_user" do 369 | 370 | before do 371 | stub_post("users/4/relationship.#{format}"). 372 | with(:body => {:action => "unblock", :access_token => @client.access_token}). 373 | to_return(:body => fixture("unblock_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 374 | end 375 | 376 | it "should get the correct resource" do 377 | @client.unblock_user(4) 378 | expect(a_post("users/4/relationship.#{format}"). 379 | with(:body => {:action => "unblock", :access_token => @client.access_token})). 380 | to have_been_made 381 | end 382 | 383 | it "should return a relationship status response" do 384 | status = @client.unblock_user(4) 385 | expect(status.outgoing_status).to eq("none") 386 | end 387 | end 388 | 389 | describe ".approve_user" do 390 | 391 | before do 392 | stub_post("users/4/relationship.#{format}"). 393 | with(:body => {:action => "approve", :access_token => @client.access_token}). 394 | to_return(:body => fixture("approve_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 395 | end 396 | 397 | it "should get the correct resource" do 398 | @client.approve_user(4) 399 | expect(a_post("users/4/relationship.#{format}"). 400 | with(:body => {:action => "approve", :access_token => @client.access_token})). 401 | to have_been_made 402 | end 403 | 404 | it "should return a relationship status response" do 405 | status = @client.approve_user(4) 406 | expect(status.outgoing_status).to eq("follows") 407 | end 408 | end 409 | 410 | describe ".deny_user" do 411 | 412 | before do 413 | stub_post("users/4/relationship.#{format}"). 414 | with(:body => {:action => "deny", :access_token => @client.access_token}). 415 | to_return(:body => fixture("deny_user.#{format}"), :headers => {:content_type => "application/#{format}; charset=utf-8"}) 416 | end 417 | 418 | it "should get the correct resource" do 419 | @client.deny_user(4) 420 | expect(a_post("users/4/relationship.#{format}"). 421 | with(:body => {:action => "deny", :access_token => @client.access_token})). 422 | to have_been_made 423 | end 424 | 425 | it "should return a relationship status response" do 426 | status = @client.deny_user(4) 427 | expect(status.outgoing_status).to eq("none") 428 | end 429 | end 430 | end 431 | end 432 | end 433 | -------------------------------------------------------------------------------- /spec/fixtures/media_search.json: -------------------------------------------------------------------------------- 1 | {"meta": {"code": 200}, "data": [{"distance": 3.9913602461968201, "type": 1, "comments": [{"created_time": "2011-01-20T12:05:13+0000", "message": "#youknowitslate when the cab driver wishes you good morning", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 20757161}, {"created_time": "2011-01-20T14:52:12+0000", "message": "Nice", "from": {"username": "newyorkcity", "full_name": "nyc ", "type": "user", "id": 1483611}, "id": 20808205}, {"created_time": "2011-01-20T18:50:02+0000", "message": "I hope you guys got some good work done :)", "from": {"username": "abelnation", "full_name": "Abel Allison", "type": "user", "id": 5315}, "id": 20873301}, {"created_time": "2011-01-20T20:54:21+0000", "message": "Hey do you follow @docpop ?Him, and his friend, made a pretty awesome Instagram Scarf.", "from": {"username": "jasonsposa", "full_name": "jason sposa", "type": "user", "id": 102516}, "id": 20900554}], "caption": {"created_time": "2011-01-20T12:05:13+0000", "message": "#youknowitslate when the cab driver wishes you good morning", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 20757161}, "like_count": 52, "link": "http://api_privatebeta.instagr.am/p/BG9It/", "user": {"username": "mikeyk", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292743625.jpg", "id": 4}, "created_time": "2011-01-20T12:04:54+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/20/6248835b0acd48d39d7ee606937ae9f7_7.jpg", "width": 612, "height": 612}}, "user_has_liked": true, "id": 18600493, "location": null}, {"distance": 4.6229071394150703, "type": 1, "comments": [{"created_time": "2010-12-18T17:25:28+0000", "message": "I like your shoes.", "from": {"username": "heather", "full_name": "Heather Millar", "type": "user", "id": 9925}, "id": 10282681}], "caption": null, "like_count": 4, "link": "http://api_privatebeta.instagr.am/p/loRA/", "user": {"username": "benbinary", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_8319_75sq_1291590245.jpg", "id": 8319}, "created_time": "2010-12-17T05:20:46+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/16/dfd3d5462fa04046bbb42476a557112a_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/16/dfd3d5462fa04046bbb42476a557112a_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/16/dfd3d5462fa04046bbb42476a557112a_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 9864256, "location": null}, {"distance": 5.4115111907653004, "type": 1, "comments": [], "caption": null, "like_count": 0, "link": "http://api_privatebeta.instagr.am/p/faLF/", "user": {"username": "kevin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295574122.jpg", "id": 3}, "created_time": "2010-12-08T00:17:36+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/07/5d3eabd192e44625b62e6df8f34ea3ff_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/07/5d3eabd192e44625b62e6df8f34ea3ff_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/07/5d3eabd192e44625b62e6df8f34ea3ff_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8233669, "location": null}, {"distance": 5.4115111907653004, "type": 1, "comments": [], "caption": null, "like_count": 0, "link": "http://api_privatebeta.instagr.am/p/f7yN/", "user": {"username": "scooterg", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_778118_75sq_1290478329.jpg", "id": 778118}, "created_time": "2010-12-08T20:47:18+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/08/b1036580b8274d5c963a9ae2e629ce4e_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/08/b1036580b8274d5c963a9ae2e629ce4e_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/08/b1036580b8274d5c963a9ae2e629ce4e_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8371341, "location": null}, {"distance": 5.8605000125634197, "type": 1, "comments": [], "caption": null, "like_count": 0, "link": "http://api_privatebeta.instagr.am/p/6FhB/", "user": {"username": "heartsf", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_814223_75sq_1295678065.jpg", "id": 814223}, "created_time": "2011-01-08T00:59:35+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/07/32a33a5980194e66a3a04dbdf974534e_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/07/32a33a5980194e66a3a04dbdf974534e_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/07/32a33a5980194e66a3a04dbdf974534e_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 15226945, "location": null}, {"distance": 6.0678124849194299, "type": 1, "comments": [{"created_time": "2010-12-10T00:16:44+0000", "message": "Instagramers", "from": {"username": "ddukes", "full_name": "Derek Dukes", "type": "user", "id": 183120}, "id": 8314181}, {"created_time": "2010-12-10T23:54:13+0000", "message": "This is so meta.", "from": {"username": "sanfranannie", "full_name": "Ann Larie Valentine", "type": "user", "id": 250245}, "id": 8510813}], "caption": {"created_time": "2010-12-10T00:16:44+0000", "message": "Instagramers", "from": {"username": "ddukes", "full_name": "Derek Dukes", "type": "user", "id": 183120}, "id": 8314181}, "like_count": 4, "link": "http://api_privatebeta.instagr.am/p/goev/", "user": {"username": "ddukes", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_183120_75sq_1295593658.jpg", "id": 183120}, "created_time": "2010-12-10T00:11:34+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/09/3e3acf87bfef42649ed76742c7fb3c00_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/09/3e3acf87bfef42649ed76742c7fb3c00_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/09/3e3acf87bfef42649ed76742c7fb3c00_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8554415, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}, {"distance": 6.2984526265725398, "type": 1, "comments": [], "caption": null, "like_count": 0, "link": "http://api_privatebeta.instagr.am/p/ezpI/", "user": {"username": "kevin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295574122.jpg", "id": 3}, "created_time": "2010-12-06T23:21:10+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/6cfb1a50a8db4885b22ff2b65d09f186_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/6cfb1a50a8db4885b22ff2b65d09f186_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/6cfb1a50a8db4885b22ff2b65d09f186_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8075848, "location": null}, {"distance": 6.2984526265725398, "type": 1, "comments": [{"created_time": "2010-12-14T06:55:23+0000", "message": "Design can change the world.", "from": {"username": "cezar", "full_name": "Robert Cezar Matei", "type": "user", "id": 3814}, "id": 9264339}, {"created_time": "2010-12-14T13:04:08+0000", "message": "Yes, I believe it can.", "from": {"username": "ckendall", "full_name": " ", "type": "user", "id": 886916}, "id": 9317114}], "caption": {"created_time": "2010-12-14T06:55:23+0000", "message": "Design can change the world.", "from": {"username": "cezar", "full_name": "Robert Cezar Matei", "type": "user", "id": 3814}, "id": 9264339}, "like_count": 6, "link": "http://api_privatebeta.instagr.am/p/jyY4/", "user": {"username": "cezar", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3814_75sq_1286386777.jpg", "id": 3814}, "created_time": "2010-12-14T06:52:24+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/13/807369b18eee459b8e1a3cba5692ca8f_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/13/807369b18eee459b8e1a3cba5692ca8f_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/13/807369b18eee459b8e1a3cba5692ca8f_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 9381432, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}, {"distance": 6.2984526265725398, "type": 1, "comments": [{"created_time": "2010-12-06T23:22:01+0000", "message": "New office #before", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": 3}, "id": 7713079}, {"created_time": "2010-12-06T23:26:06+0000", "message": "And...?", "from": {"username": "thechrisbailey", "full_name": "chris bailey", "type": "user", "id": 6254}, "id": 7713522}, {"created_time": "2010-12-06T23:32:51+0000", "message": "I don't see office space big enough for my desk and extra fluffy lounge chairs. However will I be able to work? haha. Congrats!", "from": {"username": "shoeprincess", "full_name": "Alicen Shoe Princess", "type": "user", "id": 171246}, "id": 7714511}, {"created_time": "2010-12-07T00:58:47+0000", "message": "Yay! Congrats", "from": {"username": "neo121", "full_name": "Evy ", "type": "user", "id": 4124}, "id": 7727630}, {"created_time": "2010-12-07T02:44:43+0000", "message": "Congrats — looks like a great physical environment for you to manage an electronic one for us. :)", "from": {"username": "mckelvey", "full_name": "David McKelvey", "type": "user", "id": 291024}, "id": 7741857}, {"created_time": "2010-12-07T03:09:46+0000", "message": "Where's the desk for the marketing consultant?", "from": {"username": "diane", "full_name": "Diane S", "type": "user", "id": 37}, "id": 7745179}, {"created_time": "2010-12-07T06:58:24+0000", "message": "Twitters old office?", "from": {"username": "woodshed", "full_name": "Chris Aldridge", "type": "user", "id": 6678}, "id": 7775186}, {"created_time": "2010-12-07T07:27:45+0000", "message": "@woodshed yep!", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": 3}, "id": 7778592}, {"created_time": "2010-12-07T08:36:09+0000", "message": "Funny just read about the move somewhere online yesterday. Hope it's a good omen for you", "from": {"username": "woodshed", "full_name": "Chris Aldridge", "type": "user", "id": 6678}, "id": 7786384}], "caption": {"created_time": "2010-12-06T23:22:01+0000", "message": "New office #before", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": 3}, "id": 7713079}, "like_count": 44, "link": "http://api_privatebeta.instagr.am/p/ezp6/", "user": {"username": "kevin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295574122.jpg", "id": 3}, "created_time": "2010-12-06T23:21:34+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/1642a2f5a62a48bb96ac3aefdbd9a9c1_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/1642a2f5a62a48bb96ac3aefdbd9a9c1_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/06/1642a2f5a62a48bb96ac3aefdbd9a9c1_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8075898, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}, {"distance": 7.2477187264414802, "type": 1, "comments": [{"created_time": "2010-11-18T23:55:44+0000", "message": "Instagram's new offices (they move in Dec 1.)", "from": {"username": "scobleizer", "full_name": "Robert Scoble", "type": "user", "id": 70}, "id": 4154260}, {"created_time": "2010-11-19T00:16:18+0000", "message": "Hope they will bring some stuff with them :)", "from": {"username": "snorre", "full_name": "Snørre ", "type": "user", "id": 292119}, "id": 4156978}, {"created_time": "2010-11-19T01:18:37+0000", "message": "Cool! And congrates to Instagram.", "from": {"username": "letslets", "full_name": "D Lets", "type": "user", "id": 110188}, "id": 4164806}, {"created_time": "2010-11-19T01:22:06+0000", "message": "To quote Liz Lemon, \"Me wants to go to there\"", "from": {"username": "jimmie", "full_name": "James A", "type": "user", "id": 4606}, "id": 4165210}, {"created_time": "2010-11-19T02:05:10+0000", "message": "Nice! Is that the old Odeo/Twitter/GetSatisfaction space?", "from": {"username": "shellen", "full_name": "Jason Shellen", "type": "user", "id": 93}, "id": 4170185}, {"created_time": "2010-11-19T03:26:55+0000", "message": "Shellen: yup!", "from": {"username": "scobleizer", "full_name": "Robert Scoble", "type": "user", "id": 70}, "id": 4179868}, {"created_time": "2010-11-19T07:33:48+0000", "message": "I love Instagram! It makes me happy.", "from": {"username": "ak_darylg", "full_name": "Daryl Griggs", "type": "user", "id": 198489}, "id": 4208876}], "caption": {"created_time": "2010-11-18T23:55:44+0000", "message": "Instagram's new offices (they move in Dec 1.)", "from": {"username": "scobleizer", "full_name": "Robert Scoble", "type": "user", "id": 70}, "id": 4154260}, "like_count": 21, "link": "http://api_privatebeta.instagr.am/p/Sre3/", "user": {"username": "scobleizer", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_70_75sq_1288376348.jpg", "id": 70}, "created_time": "2010-11-18T23:54:56+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/11/18/fad32e71365844d6b2fa60ce7521d4ec_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/11/18/fad32e71365844d6b2fa60ce7521d4ec_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/11/18/fad32e71365844d6b2fa60ce7521d4ec_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 4896695, "location": {"latitude": 37.78164992591033, "id": 114, "longitude": -122.3938992619514, "name": "South Park"}}, {"distance": 7.42412200877546, "type": 1, "comments": [{"created_time": "2011-01-27T01:24:19+0000", "message": "Photo shoot! @Kevin poses", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 23342324}, {"created_time": "2011-01-27T02:51:54+0000", "message": "What are the pix for?", "from": {"username": "diane", "full_name": "Diane S", "type": "user", "id": 37}, "id": 23366550}, {"created_time": "2011-01-27T02:53:08+0000", "message": "Something very cool about that harsh contrast.", "from": {"username": "truncale", "full_name": "Michael Angelo Truncale", "type": "user", "id": 319384}, "id": 23366913}, {"created_time": "2011-01-27T03:43:00+0000", "message": "I am conflicted. Will the pro photographer rely on an Instagram filter or his/her own skilz?", "from": {"username": "jtag", "full_name": "Jesse Taggert", "type": "user", "id": 209826}, "id": 23382049}, {"created_time": "2011-01-27T03:54:08+0000", "message": "His face almost looks \"one\" with the wall with the bright light!!", "from": {"username": "verona0143", "full_name": "Candice ", "type": "user", "id": 1397190}, "id": 23385644}], "caption": {"created_time": "2011-01-27T01:24:19+0000", "message": "Photo shoot! @Kevin poses", "from": {"username": "mikeyk", "full_name": "Mike Krieger Krieger", "type": "user", "id": 4}, "id": 23342324}, "like_count": 81, "link": "http://api_privatebeta.instagr.am/p/BOWzd/", "user": {"username": "mikeyk", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292743625.jpg", "id": 4}, "created_time": "2011-01-27T01:24:07+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/cf60d012a5d146ec9b2de0b74b2ab3b1_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/cf60d012a5d146ec9b2de0b74b2ab3b1_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/cf60d012a5d146ec9b2de0b74b2ab3b1_7.jpg", "width": 612, "height": 612}}, "user_has_liked": true, "id": 20540637, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}, {"distance": 7.42412200877546, "type": 1, "comments": [], "caption": null, "like_count": 0, "link": "http://api_privatebeta.instagr.am/p/BOWrk/", "user": {"username": "mikeyk", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292743625.jpg", "id": 4}, "created_time": "2011-01-27T01:21:19+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/0134470699a14261bfa6beb9a834d1dc_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/0134470699a14261bfa6beb9a834d1dc_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2011/01/26/0134470699a14261bfa6beb9a834d1dc_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 20540132, "location": null}, {"distance": 8.2377920072386406, "type": 1, "comments": [{"created_time": "2010-12-11T02:15:20+0000", "message": "Working hard! This team rocks!", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": 3}, "id": 8533746}, {"created_time": "2010-12-11T02:16:43+0000", "message": "you guys all rock!! cc: @mikeyk", "from": {"username": "pagsf", "full_name": "Pat G", "type": "user", "id": 86391}, "id": 8533951}, {"created_time": "2010-12-11T02:20:55+0000", "message": "Apple love  ", "from": {"username": "jancolors", "full_name": "Jancolors  ", "type": "user", "id": 193474}, "id": 8534620}, {"created_time": "2010-12-11T02:21:17+0000", "message": "Love!", "from": {"username": "love2snap", "full_name": "Jessica Anne", "type": "user", "id": 289485}, "id": 8534672}, {"created_time": "2010-12-11T02:21:40+0000", "message": "Thanks again for the update!!", "from": {"username": "jancolors", "full_name": "Jancolors  ", "type": "user", "id": 193474}, "id": 8534739}, {"created_time": "2010-12-11T02:24:24+0000", "message": "Cool pic!", "from": {"username": "miki1908", "full_name": "Miki Delane", "type": "user", "id": 17440}, "id": 8535171}, {"created_time": "2010-12-11T02:26:12+0000", "message": "", "from": {"username": "neo121", "full_name": "Evy ", "type": "user", "id": 4124}, "id": 8535451}, {"created_time": "2010-12-11T02:34:10+0000", "message": "Great job!", "from": {"username": "darlajmp", "full_name": "Darla Powell", "type": "user", "id": 214989}, "id": 8536686}, {"created_time": "2010-12-11T02:35:23+0000", "message": "Wow! I like this.", "from": {"username": "omar", "full_name": "Omar Kamal", "type": "user", "id": 776}, "id": 8536874}, {"created_time": "2010-12-11T02:43:38+0000", "message": "Keep going!", "from": {"username": "outsider", "full_name": "Marco ", "type": "user", "id": 373337}, "id": 8538249}, {"created_time": "2010-12-11T02:49:40+0000", "message": "Nice job !", "from": {"username": "keithmtb", "full_name": "Keith N. ", "type": "user", "id": 344904}, "id": 8539348}, {"created_time": "2010-12-11T03:00:38+0000", "message": "Great update!!", "from": {"username": "megaera", "full_name": "Teresa C", "type": "user", "id": 207272}, "id": 8541225}, {"created_time": "2010-12-11T03:07:06+0000", "message": "Cool", "from": {"username": "sarabbit", "full_name": "Sara Lee", "type": "user", "id": 99161}, "id": 8542205}, {"created_time": "2010-12-11T03:08:34+0000", "message": "Love!", "from": {"username": "armisung", "full_name": " ", "type": "user", "id": 980187}, "id": 8542441}, {"created_time": "2010-12-11T03:21:30+0000", "message": "Go go go", "from": {"username": "brianng", "full_name": "Brian Ng", "type": "user", "id": 10102}, "id": 8544564}, {"created_time": "2010-12-11T03:42:33+0000", "message": "Cool", "from": {"username": "yamak", "full_name": "Kaoru Yamada", "type": "user", "id": 185693}, "id": 8548193}, {"created_time": "2010-12-11T03:43:24+0000", "message": "iLike! How do I get on popular page?! :P", "from": {"username": "natasharochelle", "full_name": "Natasha Fischer", "type": "user", "id": 624749}, "id": 8548351}, {"created_time": "2010-12-11T03:48:32+0000", "message": "I need that job!!!", "from": {"username": "connielee", "full_name": "Connie Lee", "type": "user", "id": 11095}, "id": 8549332}, {"created_time": "2010-12-11T04:18:57+0000", "message": "Intensity @ work. Nice capture.", "from": {"username": "judithgay", "full_name": "Judith Gay Sanchez ", "type": "user", "id": 963182}, "id": 8554426}, {"created_time": "2010-12-11T19:55:11+0000", "message": "Work harder:)", "from": {"username": "fashion", "full_name": "Mal Sherlock", "type": "user", "id": 123395}, "id": 8705468}, {"created_time": "2010-12-13T04:19:13+0000", "message": "Hiring? I'll bring my own Mac, IT training, creative mind, and passion for photography! :)", "from": {"username": "alt", "full_name": "amanda lee", "type": "user", "id": 631919}, "id": 9026941}], "caption": {"created_time": "2010-12-11T02:15:20+0000", "message": "Working hard! This team rocks!", "from": {"username": "kevin", "full_name": "Kevin Systrom", "type": "user", "id": 3}, "id": 8533746}, "like_count": 95, "link": "http://api_privatebeta.instagr.am/p/hWrm/", "user": {"username": "kevin", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295574122.jpg", "id": 3}, "created_time": "2010-12-11T02:14:15+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/10/e6c53ef7eac84e27bb29583e41330021_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/10/e6c53ef7eac84e27bb29583e41330021_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/10/e6c53ef7eac84e27bb29583e41330021_7.jpg", "width": 612, "height": 612}}, "user_has_liked": true, "id": 8743654, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}, {"distance": 8.2447868947217398, "type": 1, "comments": [{"created_time": "2010-12-11T22:46:10+0000", "message": "Skype with my boys", "from": {"username": "shayne", "full_name": "Shayne Sweeney", "type": "user", "id": 20}, "id": 8729343}, {"created_time": "2010-12-11T22:54:07+0000", "message": "That's cute!", "from": {"username": "pmh360", "full_name": "PMH ", "type": "user", "id": 86585}, "id": 8730658}, {"created_time": "2010-12-12T00:07:43+0000", "message": "Love him!!", "from": {"username": "k_ladyhawk", "full_name": "Kristi Velasquez ", "type": "user", "id": 820795}, "id": 8743507}, {"created_time": "2010-12-12T23:07:45+0000", "message": "Such a cutie!", "from": {"username": "lduncan", "full_name": "Lebria Duncan", "type": "user", "id": 612793}, "id": 8977484}, {"created_time": "2010-12-16T05:19:09+0000", "message": "Very cute", "from": {"username": "missy35", "full_name": " ", "type": "user", "id": 805710}, "id": 9697658}], "caption": {"created_time": "2010-12-11T22:46:10+0000", "message": "Skype with my boys", "from": {"username": "shayne", "full_name": "Shayne Sweeney", "type": "user", "id": 20}, "id": 8729343}, "like_count": 8, "link": "http://api_privatebeta.instagr.am/p/iEtt/", "user": {"username": "shayne", "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_20_75sq_1290558237.jpg", "id": 20}, "created_time": "2010-12-11T22:45:52+0000", "images": {"low_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/11/4c462809cbba4f25a39a829b79a5d003_6.jpg", "width": 480, "height": 480}, "thumbnail": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/11/4c462809cbba4f25a39a829b79a5d003_5.jpg", "width": 150, "height": 150}, "high_resolution": {"url": "http://distillery.s3.amazonaws.com/media/2010/12/11/4c462809cbba4f25a39a829b79a5d003_7.jpg", "width": 612, "height": 612}}, "user_has_liked": false, "id": 8932205, "location": {"latitude": 37.780885099999999, "id": 514276, "longitude": -122.3948632, "name": "Instagram"}}]} --------------------------------------------------------------------------------