instagram-community-maintained 1.1.6

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 +7 -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 +261 -0
  10. data/Rakefile +27 -0
  11. data/instagram.gemspec +53 -0
  12. data/lib/faraday/loud_logger.rb +75 -0
  13. data/lib/faraday/oauth2.rb +42 -0
  14. data/lib/faraday/raise_http_exception.rb +59 -0
  15. data/lib/instagram.rb +27 -0
  16. data/lib/instagram/api.rb +31 -0
  17. data/lib/instagram/client.rb +21 -0
  18. data/lib/instagram/client/comments.rb +62 -0
  19. data/lib/instagram/client/embedding.rb +28 -0
  20. data/lib/instagram/client/geographies.rb +29 -0
  21. data/lib/instagram/client/likes.rb +58 -0
  22. data/lib/instagram/client/locations.rb +75 -0
  23. data/lib/instagram/client/media.rb +82 -0
  24. data/lib/instagram/client/subscriptions.rb +211 -0
  25. data/lib/instagram/client/tags.rb +59 -0
  26. data/lib/instagram/client/users.rb +310 -0
  27. data/lib/instagram/client/utils.rb +28 -0
  28. data/lib/instagram/configuration.rb +122 -0
  29. data/lib/instagram/connection.rb +31 -0
  30. data/lib/instagram/error.rb +31 -0
  31. data/lib/instagram/oauth.rb +36 -0
  32. data/lib/instagram/request.rb +82 -0
  33. data/lib/instagram/response.rb +21 -0
  34. data/lib/instagram/version.rb +3 -0
  35. data/spec/faraday/response_spec.rb +86 -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_fsq.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 +424 -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 +328 -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,122 @@
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
+ :loud_logger,
23
+ :sign_requests,
24
+ ].freeze
25
+
26
+ # By default, don't set a user access token
27
+ DEFAULT_ACCESS_TOKEN = nil
28
+
29
+ # The adapter that will be used to connect if none is set
30
+ #
31
+ # @note The default faraday adapter is Net::HTTP.
32
+ DEFAULT_ADAPTER = Faraday.default_adapter
33
+
34
+ # By default, don't set an application ID
35
+ DEFAULT_CLIENT_ID = nil
36
+
37
+ # By default, don't set an application secret
38
+ DEFAULT_CLIENT_SECRET = nil
39
+
40
+ # By default, don't set application IPs
41
+ DEFAULT_CLIENT_IPS = nil
42
+
43
+ # By default, don't set any connection options
44
+ DEFAULT_CONNECTION_OPTIONS = {}
45
+
46
+ # The endpoint that will be used to connect if none is set
47
+ #
48
+ # @note There is no reason to use any other endpoint at this time
49
+ DEFAULT_ENDPOINT = 'https://api.instagram.com/v1/'.freeze
50
+
51
+ # The response format appended to the path and sent in the 'Accept' header if none is set
52
+ #
53
+ # @note JSON is the only available format at this time
54
+ DEFAULT_FORMAT = :json
55
+
56
+ # By default, don't use a proxy server
57
+ DEFAULT_PROXY = nil
58
+
59
+ # By default, don't set an application redirect uri
60
+ DEFAULT_REDIRECT_URI = nil
61
+
62
+ # By default, don't set a user scope
63
+ DEFAULT_SCOPE = nil
64
+
65
+ # By default, don't wrap responses with meta data (i.e. pagination)
66
+ DEFAULT_NO_RESPONSE_WRAPPER = false
67
+
68
+ # The user agent that will be sent to the API endpoint if none is set
69
+ DEFAULT_USER_AGENT = "Instagram Ruby Gem #{Instagram::VERSION}".freeze
70
+
71
+ # An array of valid request/response formats
72
+ #
73
+ # @note Not all methods support the XML format.
74
+ VALID_FORMATS = [
75
+ :json].freeze
76
+
77
+ # By default, don't turn on loud logging
78
+ DEFAULT_LOUD_LOGGER = nil
79
+
80
+ # By default, requests are not signed
81
+ DEFAULT_SIGN_REQUESTS = false
82
+
83
+ # @private
84
+ attr_accessor *VALID_OPTIONS_KEYS
85
+
86
+ # When this module is extended, set all configuration options to their default values
87
+ def self.extended(base)
88
+ base.reset
89
+ end
90
+
91
+ # Convenience method to allow configuration options to be set in a block
92
+ def configure
93
+ yield self
94
+ end
95
+
96
+ # Create a hash of options and their values
97
+ def options
98
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
99
+ option.merge!(key => send(key))
100
+ end
101
+ end
102
+
103
+ # Reset all configuration options to defaults
104
+ def reset
105
+ self.access_token = DEFAULT_ACCESS_TOKEN
106
+ self.adapter = DEFAULT_ADAPTER
107
+ self.client_id = DEFAULT_CLIENT_ID
108
+ self.client_secret = DEFAULT_CLIENT_SECRET
109
+ self.client_ips = DEFAULT_CLIENT_IPS
110
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
111
+ self.scope = DEFAULT_SCOPE
112
+ self.redirect_uri = DEFAULT_REDIRECT_URI
113
+ self.endpoint = DEFAULT_ENDPOINT
114
+ self.format = DEFAULT_FORMAT
115
+ self.proxy = DEFAULT_PROXY
116
+ self.user_agent = DEFAULT_USER_AGENT
117
+ self.no_response_wrapper= DEFAULT_NO_RESPONSE_WRAPPER
118
+ self.loud_logger = DEFAULT_LOUD_LOGGER
119
+ self.sign_requests = DEFAULT_SIGN_REQUESTS
120
+ end
121
+ end
122
+ 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,31 @@
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
@@ -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,82 @@
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.sort.map do |key, val|
75
+ sig += '|%s=%s' % [key, val]
76
+ end
77
+ digest = OpenSSL::Digest::Digest.new('sha256')
78
+ return OpenSSL::HMAC.hexdigest(digest, secret, sig)
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,3 @@
1
+ module Instagram
2
+ VERSION = '1.1.6'.freeze unless defined?(::Instagram::VERSION)
3
+ end
@@ -0,0 +1,86 @@
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
+ lambda do
24
+ @client.user_media_feed()
25
+ end.should raise_error(exception)
26
+ end
27
+
28
+ end
29
+ end
30
+
31
+ context "when a 400 is raised" do
32
+ before do
33
+ stub_get('users/self/feed.json').
34
+ to_return(:body => '{"meta":{"error_message": "Bad words are bad."}}', :status => 400)
35
+ end
36
+
37
+ it "should return the body error message" do
38
+ expect do
39
+ @client.user_media_feed()
40
+ end.to raise_error(Instagram::BadRequest, /Bad words are bad\./)
41
+ end
42
+ end
43
+
44
+ context "when a 400 is raised with no meta but an error_message" do
45
+ before do
46
+ stub_get('users/self/feed.json').
47
+ to_return(:body => '{"error_type": "OAuthException", "error_message": "No matching code found."}', :status => 400)
48
+ end
49
+
50
+ it "should return the body error type and message" do
51
+ expect do
52
+ @client.user_media_feed()
53
+ end.to raise_error(Instagram::BadRequest, /OAuthException: No matching code found\./)
54
+ end
55
+ end
56
+
57
+ context 'when a 502 is raised with an HTML response' do
58
+ before do
59
+ stub_get('users/self/feed.json').to_return(
60
+ :body => '<html><body><h1>502 Bad Gateway</h1> The server returned an invalid or incomplete response. </body></html>',
61
+ :status => 502
62
+ )
63
+ end
64
+
65
+ it 'should raise an Instagram::BadGateway' do
66
+ lambda do
67
+ @client.user_media_feed()
68
+ end.should raise_error(Instagram::BadGateway)
69
+ end
70
+ end
71
+
72
+ context 'when a 504 is raised with an HTML response' do
73
+ before do
74
+ stub_get('users/self/feed.json').to_return(
75
+ :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>',
76
+ :status => 504
77
+ )
78
+ end
79
+
80
+ it 'should raise an Instagram::GatewayTimeout' do
81
+ lambda do
82
+ @client.user_media_feed()
83
+ end.should raise_error(Instagram::GatewayTimeout)
84
+ end
85
+ end
86
+ end