instagram-community-maintained 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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