extendi-instagram 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/.yardopts +9 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +30 -0
  8. data/PATENTS.md +23 -0
  9. data/README.md +260 -0
  10. data/Rakefile +27 -0
  11. data/instagram.gemspec +50 -0
  12. data/lib/faraday/loud_logger.rb +78 -0
  13. data/lib/faraday/oauth2.rb +45 -0
  14. data/lib/faraday/raise_http_exception.rb +73 -0
  15. data/lib/instagram/api.rb +31 -0
  16. data/lib/instagram/client/comments.rb +62 -0
  17. data/lib/instagram/client/embedding.rb +28 -0
  18. data/lib/instagram/client/geographies.rb +29 -0
  19. data/lib/instagram/client/likes.rb +58 -0
  20. data/lib/instagram/client/locations.rb +75 -0
  21. data/lib/instagram/client/media.rb +82 -0
  22. data/lib/instagram/client/subscriptions.rb +211 -0
  23. data/lib/instagram/client/tags.rb +59 -0
  24. data/lib/instagram/client/users.rb +310 -0
  25. data/lib/instagram/client/utils.rb +28 -0
  26. data/lib/instagram/client.rb +21 -0
  27. data/lib/instagram/configuration.rb +125 -0
  28. data/lib/instagram/connection.rb +31 -0
  29. data/lib/instagram/error.rb +34 -0
  30. data/lib/instagram/oauth.rb +36 -0
  31. data/lib/instagram/request.rb +83 -0
  32. data/lib/instagram/response.rb +22 -0
  33. data/lib/instagram/version.rb +3 -0
  34. data/lib/instagram.rb +27 -0
  35. data/spec/faraday/response_spec.rb +101 -0
  36. data/spec/fixtures/access_token.json +9 -0
  37. data/spec/fixtures/approve_user.json +8 -0
  38. data/spec/fixtures/block_user.json +8 -0
  39. data/spec/fixtures/deny_user.json +8 -0
  40. data/spec/fixtures/follow_user.json +8 -0
  41. data/spec/fixtures/followed_by.json +1 -0
  42. data/spec/fixtures/follows.json +1 -0
  43. data/spec/fixtures/geography_recent_media.json +1 -0
  44. data/spec/fixtures/liked_media.json +1 -0
  45. data/spec/fixtures/location.json +1 -0
  46. data/spec/fixtures/location_recent_media.json +1 -0
  47. data/spec/fixtures/location_search.json +1 -0
  48. data/spec/fixtures/location_search_facebook.json +1 -0
  49. data/spec/fixtures/media.json +1 -0
  50. data/spec/fixtures/media_comment.json +1 -0
  51. data/spec/fixtures/media_comment_deleted.json +1 -0
  52. data/spec/fixtures/media_comments.json +1 -0
  53. data/spec/fixtures/media_liked.json +1 -0
  54. data/spec/fixtures/media_likes.json +1 -0
  55. data/spec/fixtures/media_popular.json +1 -0
  56. data/spec/fixtures/media_search.json +1 -0
  57. data/spec/fixtures/media_shortcode.json +1 -0
  58. data/spec/fixtures/media_unliked.json +1 -0
  59. data/spec/fixtures/mikeyk.json +1 -0
  60. data/spec/fixtures/oembed.json +14 -0
  61. data/spec/fixtures/recent_media.json +1 -0
  62. data/spec/fixtures/relationship.json +9 -0
  63. data/spec/fixtures/requested_by.json +12 -0
  64. data/spec/fixtures/shayne.json +1 -0
  65. data/spec/fixtures/subscription.json +12 -0
  66. data/spec/fixtures/subscription_deleted.json +1 -0
  67. data/spec/fixtures/subscription_payload.json +14 -0
  68. data/spec/fixtures/subscriptions.json +22 -0
  69. data/spec/fixtures/tag.json +1 -0
  70. data/spec/fixtures/tag_recent_media.json +1 -0
  71. data/spec/fixtures/tag_search.json +1 -0
  72. data/spec/fixtures/unblock_user.json +8 -0
  73. data/spec/fixtures/unfollow_user.json +8 -0
  74. data/spec/fixtures/user_media_feed.json +1 -0
  75. data/spec/fixtures/user_search.json +1 -0
  76. data/spec/instagram/api_spec.rb +285 -0
  77. data/spec/instagram/client/comments_spec.rb +71 -0
  78. data/spec/instagram/client/embedding_spec.rb +36 -0
  79. data/spec/instagram/client/geography_spec.rb +37 -0
  80. data/spec/instagram/client/likes_spec.rb +66 -0
  81. data/spec/instagram/client/locations_spec.rb +127 -0
  82. data/spec/instagram/client/media_spec.rb +99 -0
  83. data/spec/instagram/client/subscriptions_spec.rb +174 -0
  84. data/spec/instagram/client/tags_spec.rb +79 -0
  85. data/spec/instagram/client/users_spec.rb +432 -0
  86. data/spec/instagram/client/utils_spec.rb +32 -0
  87. data/spec/instagram/client_spec.rb +23 -0
  88. data/spec/instagram/request_spec.rb +56 -0
  89. data/spec/instagram_spec.rb +109 -0
  90. data/spec/spec_helper.rb +71 -0
  91. metadata +322 -0
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,125 @@
1
+ require 'faraday'
2
+ #if using typhoeus as the adapter uncomment these two requires to avoid seeing "Ethon::Errors::InvalidOption: The option: disable_ssl_peer_verification is invalid." (https://github.com/typhoeus/typhoeus/issues/270)
3
+ #require 'typhoeus'
4
+ #require 'typhoeus/adapters/faraday'
5
+ require File.expand_path('../version', __FILE__)
6
+
7
+ module Instagram
8
+ # Defines constants and methods related to configuration
9
+ module Configuration
10
+ # An array of valid keys in the options hash when configuring a {Instagram::API}
11
+ VALID_OPTIONS_KEYS = [
12
+ :access_token,
13
+ :adapter,
14
+ :client_id,
15
+ :client_secret,
16
+ :client_ips,
17
+ :connection_options,
18
+ :scope,
19
+ :redirect_uri,
20
+ :endpoint,
21
+ :format,
22
+ :proxy,
23
+ :user_agent,
24
+ :no_response_wrapper,
25
+ :loud_logger,
26
+ :sign_requests,
27
+ ].freeze
28
+
29
+ # By default, don't set a user access token
30
+ DEFAULT_ACCESS_TOKEN = nil
31
+
32
+ # The adapter that will be used to connect if none is set
33
+ #
34
+ # @note The default faraday adapter is Net::HTTP.
35
+ DEFAULT_ADAPTER = Faraday.default_adapter
36
+
37
+ # By default, don't set an application ID
38
+ DEFAULT_CLIENT_ID = nil
39
+
40
+ # By default, don't set an application secret
41
+ DEFAULT_CLIENT_SECRET = nil
42
+
43
+ # By default, don't set application IPs
44
+ DEFAULT_CLIENT_IPS = nil
45
+
46
+ # By default, don't set any connection options
47
+ DEFAULT_CONNECTION_OPTIONS = {}
48
+
49
+ # The endpoint that will be used to connect if none is set
50
+ #
51
+ # @note There is no reason to use any other endpoint at this time
52
+ DEFAULT_ENDPOINT = 'https://api.instagram.com/v1/'.freeze
53
+
54
+ # The response format appended to the path and sent in the 'Accept' header if none is set
55
+ #
56
+ # @note JSON is the only available format at this time
57
+ DEFAULT_FORMAT = :json
58
+
59
+ # By default, don't use a proxy server
60
+ DEFAULT_PROXY = nil
61
+
62
+ # By default, don't set an application redirect uri
63
+ DEFAULT_REDIRECT_URI = nil
64
+
65
+ # By default, don't set a user scope
66
+ DEFAULT_SCOPE = nil
67
+
68
+ # By default, don't wrap responses with meta data (i.e. pagination)
69
+ DEFAULT_NO_RESPONSE_WRAPPER = false
70
+
71
+ # The user agent that will be sent to the API endpoint if none is set
72
+ DEFAULT_USER_AGENT = "Instagram Ruby Gem #{Instagram::VERSION}".freeze
73
+
74
+ # An array of valid request/response formats
75
+ #
76
+ # @note Not all methods support the XML format.
77
+ VALID_FORMATS = [
78
+ :json].freeze
79
+
80
+ # By default, don't turn on loud logging
81
+ DEFAULT_LOUD_LOGGER = nil
82
+
83
+ # By default, requests are not signed
84
+ DEFAULT_SIGN_REQUESTS = false
85
+
86
+ # @private
87
+ attr_accessor *VALID_OPTIONS_KEYS
88
+
89
+ # When this module is extended, set all configuration options to their default values
90
+ def self.extended(base)
91
+ base.reset
92
+ end
93
+
94
+ # Convenience method to allow configuration options to be set in a block
95
+ def configure
96
+ yield self
97
+ end
98
+
99
+ # Create a hash of options and their values
100
+ def options
101
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
102
+ option.merge!(key => send(key))
103
+ end
104
+ end
105
+
106
+ # Reset all configuration options to defaults
107
+ def reset
108
+ self.access_token = DEFAULT_ACCESS_TOKEN
109
+ self.adapter = DEFAULT_ADAPTER
110
+ self.client_id = DEFAULT_CLIENT_ID
111
+ self.client_secret = DEFAULT_CLIENT_SECRET
112
+ self.client_ips = DEFAULT_CLIENT_IPS
113
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
114
+ self.scope = DEFAULT_SCOPE
115
+ self.redirect_uri = DEFAULT_REDIRECT_URI
116
+ self.endpoint = DEFAULT_ENDPOINT
117
+ self.format = DEFAULT_FORMAT
118
+ self.proxy = DEFAULT_PROXY
119
+ self.user_agent = DEFAULT_USER_AGENT
120
+ self.no_response_wrapper= DEFAULT_NO_RESPONSE_WRAPPER
121
+ self.loud_logger = DEFAULT_LOUD_LOGGER
122
+ self.sign_requests = DEFAULT_SIGN_REQUESTS
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,31 @@
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.use FaradayMiddleware::LoudLogger if loud_logger
27
+ connection.adapter(adapter)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
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 403
9
+ class Forbidden < Error; end
10
+
11
+ # Raised when Instagram returns the HTTP status code 404
12
+ class NotFound < Error; end
13
+
14
+ # Raised when Instagram returns the HTTP status code 429
15
+ class TooManyRequests < Error; end
16
+
17
+ # Raised when Instagram returns the HTTP status code 500
18
+ class InternalServerError < Error; end
19
+
20
+ # Raised when Instagram returns the HTTP status code 502
21
+ class BadGateway < Error; end
22
+
23
+ # Raised when Instagram returns the HTTP status code 503
24
+ class ServiceUnavailable < Error; end
25
+
26
+ # Raised when Instagram returns the HTTP status code 504
27
+ class GatewayTimeout < Error; end
28
+
29
+ # Raised when a subscription payload hash is invalid
30
+ class InvalidSignature < Error; end
31
+
32
+ # Raised when Instagram returns the HTTP status code 429
33
+ class RateLimitExceeded < Error; end
34
+ end
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,83 @@
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(), signed=sign_requests)
9
+ request(:get, path, options, signature, raw, unformatted, no_response_wrapper, signed)
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(), signed=sign_requests)
14
+ request(:post, path, options, signature, raw, unformatted, no_response_wrapper, signed)
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(), signed=sign_requests)
19
+ request(:put, path, options, signature, raw, unformatted, no_response_wrapper, signed)
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(), signed=sign_requests)
24
+ request(:delete, path, options, signature, raw, unformatted, no_response_wrapper, signed)
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, signed=sign_requests)
31
+ response = connection(raw).send(method) do |request|
32
+ path = formatted_path(path) unless unformatted
33
+
34
+ if signed == true
35
+ if client_id != nil
36
+ sig_options = options.merge({:client_id => client_id})
37
+ end
38
+ if access_token != nil
39
+ sig_options = options.merge({:access_token => access_token})
40
+ end
41
+ sig = generate_sig("/"+path, sig_options, client_secret)
42
+ options[:sig] = sig
43
+ end
44
+
45
+ case method
46
+ when :get, :delete
47
+ request.url(URI.encode(path), options)
48
+ when :post, :put
49
+ request.path = URI.encode(path)
50
+ request.body = options unless options.empty?
51
+ end
52
+ if signature && client_ips != nil
53
+ request.headers["X-Insta-Forwarded-For"] = get_insta_fowarded_for(client_ips, client_secret)
54
+ end
55
+ end
56
+ return response if raw
57
+ return response.body if no_response_wrapper
58
+ return Response.create( response.body, {:limit => response.headers['x-ratelimit-limit'].to_i,
59
+ :remaining => response.headers['x-ratelimit-remaining'].to_i} )
60
+ end
61
+
62
+ def formatted_path(path)
63
+ [path, format].compact.join('.')
64
+ end
65
+
66
+ def get_insta_fowarded_for(ips, secret)
67
+ digest = OpenSSL::Digest.new('sha256')
68
+ signature = OpenSSL::HMAC.hexdigest(digest, secret, ips)
69
+ return [ips, signature].join('|')
70
+ end
71
+
72
+ def generate_sig(endpoint, params, secret)
73
+ sig = endpoint
74
+ params = params.sort_by{|c|c[0].to_s}
75
+ params.map do |key, val|
76
+ sig += '|%s=%s' % [key, val]
77
+ end
78
+ digest = OpenSSL::Digest.new('sha256')
79
+ return OpenSSL::HMAC.hexdigest(digest, secret, sig)
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,22 @@
1
+ module Instagram
2
+ module Response
3
+ def self.create( response_hash, ratelimit_hash )
4
+ response_hash = {} unless response_hash
5
+ data = response_hash.data.dup rescue response_hash
6
+ data.extend( self )
7
+ data.instance_exec do
8
+ %w{pagination meta}.each do |k|
9
+ response_hash.public_send(k).tap do |v|
10
+ instance_variable_set("@#{k}", v) if v
11
+ end
12
+ end
13
+ @ratelimit = ::Hashie::Mash.new(ratelimit_hash)
14
+ end
15
+ data
16
+ end
17
+
18
+ attr_reader :pagination
19
+ attr_reader :meta
20
+ attr_reader :ratelimit
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Instagram
2
+ VERSION = '2.0.0'.freeze unless defined?(::Instagram::VERSION)
3
+ end
data/lib/instagram.rb ADDED
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,101 @@
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
+ 403 => Instagram::Forbidden,
11
+ 404 => Instagram::NotFound,
12
+ 429 => Instagram::TooManyRequests,
13
+ 500 => Instagram::InternalServerError,
14
+ 503 => Instagram::ServiceUnavailable
15
+ }.each do |status, exception|
16
+ context "when HTTP status is #{status}" do
17
+
18
+ before do
19
+ stub_get('users/self/feed.json').
20
+ to_return(:status => status)
21
+ end
22
+
23
+ it "should raise #{exception.name} error" do
24
+ expect do
25
+ @client.user_media_feed()
26
+ end.to raise_error(exception)
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ context "when a 400 is raised" do
33
+ before do
34
+ stub_get('users/self/feed.json').
35
+ to_return(:body => '{"meta":{"error_message": "Bad words are bad."}}', :status => 400)
36
+ end
37
+
38
+ it "should return the body error message" do
39
+ expect do
40
+ @client.user_media_feed()
41
+ end.to raise_error(Instagram::BadRequest, /Bad words are bad\./)
42
+ end
43
+ end
44
+
45
+ context "when a 400 is raised with no meta but an error_message" do
46
+ before do
47
+ stub_get('users/self/feed.json').
48
+ to_return(:body => '{"error_type": "OAuthException", "error_message": "No matching code found."}', :status => 400)
49
+ end
50
+
51
+ it "should return the body error type and message" do
52
+ expect do
53
+ @client.user_media_feed()
54
+ end.to raise_error(Instagram::BadRequest, /OAuthException: No matching code found\./)
55
+ end
56
+ end
57
+
58
+ context "when a 400 is raised with an HTML response" do
59
+ before do
60
+ stub_get('users/self/feed.json').to_return(
61
+ :body => '<html><body><h1>400 Bad Request</h1> The server returned an invalid or incomplete response. </body></html>',
62
+ :status => 400)
63
+ end
64
+
65
+ it "should return the body error type" do
66
+ expect do
67
+ @client.user_media_feed()
68
+ end.to raise_error(Instagram::BadRequest)
69
+ end
70
+ end
71
+
72
+ context 'when a 502 is raised with an HTML response' do
73
+ before do
74
+ stub_get('users/self/feed.json').to_return(
75
+ :body => '<html><body><h1>502 Bad Gateway</h1> The server returned an invalid or incomplete response. </body></html>',
76
+ :status => 502
77
+ )
78
+ end
79
+
80
+ it 'should raise an Instagram::BadGateway' do
81
+ expect do
82
+ @client.user_media_feed()
83
+ end.to raise_error(Instagram::BadGateway)
84
+ end
85
+ end
86
+
87
+ context 'when a 504 is raised with an HTML response' do
88
+ before do
89
+ stub_get('users/self/feed.json').to_return(
90
+ :body => '<html> <head><title>504 Gateway Time-out</title></head> <body bgcolor="white"> <center><h1>504 Gateway Time-out</h1></center> <hr><center>nginx</center> </body> </html>',
91
+ :status => 504
92
+ )
93
+ end
94
+
95
+ it 'should raise an Instagram::GatewayTimeout' do
96
+ expect do
97
+ @client.user_media_feed()
98
+ end.to raise_error(Instagram::GatewayTimeout)
99
+ end
100
+ end
101
+ end