diff --git a/lib/faraday/raise_http_exception.rb b/lib/faraday/raise_http_exception.rb index ba06a81..2399f2e 100644 --- a/lib/faraday/raise_http_exception.rb +++ b/lib/faraday/raise_http_exception.rb @@ -4,11 +4,23 @@ module FaradayMiddleware # @private class RaiseHttpException < Faraday::Middleware + TOKEN_EXPIRED_ERROR_CODES = { + '190' => 'Access token has expired', + '460' => 'Password Changed', + '463' => 'Login status or access token has expired, been revoked, or is otherwise invalid', + '467' => 'Access token has expired, been revoked, or is otherwise invalid', + '492' => 'Invalid Session' + } + def call(env) @app.call(env).on_complete do |response| case response[:status].to_i when 400 - raise Instagram::BadRequest, error_message_400(response) + if access_token_expired?(response) + raise Instagram::AccessTokenExpired, error_message_400(response, token_expired_message(response)) + else + raise Instagram::BadRequest, error_message_400(response, 'Invalid OAuth access token') + end when 404 raise Instagram::NotFound, error_message_400(response) when 429 @@ -32,28 +44,58 @@ def initialize(app) private - def error_message_400(response) - "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}#{error_body(response[:body])}" + def token_expired_message(response) + TOKEN_EXPIRED_ERROR_CODES[error_code(response)] + end + + def access_token_expired?(response) + body = error_body(response[:body]) + + if body.present? + code = error_code(response) + if code.present? + return TOKEN_EXPIRED_ERROR_CODES[code.to_s].present? + end + end + + false + end + + def error_message_400(response, body=nil) + "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', error_body_message(response), body].compact.join(' ')}" + end + + def error_message_500(response, body=nil) + "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', error_body_message(response), body].compact.join(' ')}" end def error_body(body) # body gets passed as a string, not sure if it is passed as something else from other spots? if not body.nil? and not body.empty? and body.kind_of?(String) # removed multi_json thanks to wesnolte's commit - body = ::JSON.parse(body) + body = ::JSON.parse(body) rescue body end + body + end + + def error_code(response) + body = error_body(response[:body]) + body.dig('error', 'code') + rescue + response.dig(:status) + end + + def error_body_message(response) + body = error_body(response['body']) + if body.nil? nil - elsif body['meta'] and body['meta']['error_message'] and not body['meta']['error_message'].empty? - ": #{body['meta']['error_message']}" - elsif body['error_message'] and not body['error_message'].empty? - ": #{body['error_type']}: #{body['error_message']}" + elsif body.is_a?(Hash) && (msg = body.dig('error', 'message')).present? + ": #{msg}" + else + ": #{body.to_s}" end end - - def error_message_500(response, body=nil) - "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', body].compact.join(' ')}" - end end end diff --git a/lib/instagram/client.rb b/lib/instagram/client.rb index 1586477..ecbca33 100644 --- a/lib/instagram/client.rb +++ b/lib/instagram/client.rb @@ -10,12 +10,5 @@ class Client < API include Instagram::Client::Users include Instagram::Client::Media - include Instagram::Client::Locations - include Instagram::Client::Geographies - include Instagram::Client::Tags - include Instagram::Client::Comments - include Instagram::Client::Likes - include Instagram::Client::Subscriptions - include Instagram::Client::Embedding end end diff --git a/lib/instagram/client/comments.rb b/lib/instagram/client/comments.rb deleted file mode 100644 index 320ccdb..0000000 --- a/lib/instagram/client/comments.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Instagram - class Client - # Defines methods related to comments - module Comments - # Returns a list of comments for a given media item ID - # - # @overload media_comments(id) - # @param id [Integer] An Instagram media item ID - # @return [Hashie::Mash] The requested comments. - # @example Returns a list of comments for the media item of ID 1234 - # Instagram.media_comments(777) - # @format :json - # @authenticated true - # - # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/comments/#get_media_comments - def media_comments(id, *args) - response = get("media/#{id}/comments") - response - end - - # Creates a comment for a given media item ID - # - # @overload create_media_comment(id, text) - # @param id [Integer] An Instagram media item ID - # @param text [String] The text of your comment - # @return [Hashie::Mash] The comment created. - # @example Creates a new comment on media item with ID 777 - # Instagram.create_media_comment(777, "Oh noes!") - # @format :json - # @authenticated true - # - # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/comments/#post_media_comments - def create_media_comment(id, text, options={}) - response = post("media/#{id}/comments", options.merge(:text => text), signature=true) - response - end - - # Deletes a comment for a given media item ID - # - # @overload delete_media_comment(media_id, comment_id) - # @param media_id [Integer] An Instagram media item ID. - # @param comment_id [Integer] Your comment ID of the comment you wish to delete. - # @return [nil] - # @example Delete the comment with ID of 1234, on the media item with ID of 777 - # Instagram.delete_media_comment(777, 1234) - # @format :json - # @authenticated true - # - # In order to remove a comment, you must be the owner of the comment, the media item, or both. - # @rate_limited true - # @see http://instagram.com/developer/endpoints/comments/#delete_media_comments - def delete_media_comment(media_id, comment_id, options={}) - response = delete("media/#{media_id}/comments/#{comment_id}", options, signature=true) - response - end - end - end -end diff --git a/lib/instagram/client/embedding.rb b/lib/instagram/client/embedding.rb deleted file mode 100644 index 2f86517..0000000 --- a/lib/instagram/client/embedding.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Instagram - class Client - # Defines methods related to embedding - module Embedding - # Returns information about the media associated with the given short link - # - # @overload oembed(url=nil, options={}) - # @param url [String] An instagram short link - # @param options [Hash] A customizable set of options - # @option options [Integer] :maxheight Maximum height of returned media - # @option options [Integer] :maxwidth Maximum width of returned media - # @option options [Integer] :callback A JSON callback to be invoked - # @return [Hashie::Mash] Information about the media associated with given short link - # @example Return information about the media associated with http://instagr.am/p/BUG/ - # Instagram.oembed(http://instagr.am/p/BUG/) - # - # @see http://instagram.com/developer/embedding/#oembed - # @format :json - # @authenticated false - # @rate_limited true - def oembed(*args) - url = args.first - return nil unless url - get("oembed?url=#{url}", {}, false, false, true) - end - end - end -end diff --git a/lib/instagram/client/geographies.rb b/lib/instagram/client/geographies.rb deleted file mode 100644 index cc0ad88..0000000 --- a/lib/instagram/client/geographies.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Instagram - class Client - # Defines methods related to real-time geographies - module Geographies - # Returns a list of recent media items for a given real-time geography - # - # @overload geography_recent_media(id, options={}) - # @param user [Integer] A geography ID from a real-time subscription. - # @param options [Hash] A customizable set of options. - # @option options [Integer] :count (nil) Limit the number of results returned - # @option options [Integer] :min_id (nil) Return media before this min_id - # @option options [Integer] :max_id (nil) Return media after this max_id - # @option options [Integer] :min_timestamp (nil) Return media after this UNIX timestamp - # @option options [Integer] :max_timestamp (nil) Return media before this UNIX timestamp - # @return [Hashie::Mash] - # @example Return a list of the most recent media items taken within a specific geography - # Instagram.geography_recent_media(514276) - # @see http://instagram.com/developer/endpoints/geographies/ - # @format :json - # @authenticated false - # @rate_limited true - def geography_recent_media(id, *args) - options = args.last.is_a?(Hash) ? args.pop : {} - response = get("geographies/#{id}/media/recent", options) - response - end - end - end -end diff --git a/lib/instagram/client/likes.rb b/lib/instagram/client/likes.rb deleted file mode 100644 index a35b374..0000000 --- a/lib/instagram/client/likes.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Instagram - class Client - # Defines methods related to likes - module Likes - # Returns a list of users who like a given media item ID - # - # @overload media_likes(id) - # @param media [Integer] An Instagram media item ID - # @return [Hashie::Mash] A list of users. - # @example Returns a list of users who like the media item of ID 1234 - # Instagram.media_likes(777) - # @format :json - # @authenticated true - # - # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/likes/#get_media_likes - def media_likes(id, *args) - response = get("media/#{id}/likes") - response - end - - # Issues a like by the currently authenticated user, for a given media item ID - # - # @overload like_media(id, text) - # @param id [Integer] An Instagram media item ID - # @return [Hashie::Mash] Metadata - # @example Like media item with ID 777 - # Instagram.like_media(777) - # @format :json - # @authenticated true - # - # If getting this data of a protected user, you must be authenticated (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/likes/#post_likes - def like_media(id, options={}) - response = post("media/#{id}/likes", options, signature=true) - response - end - - # Removes the like on a givem media item ID for the currently authenticated user - # - # @overload unlike_media(id) - # @param media_id [Integer] An Instagram media item ID. - # @return [Hashie::Mash] Metadata - # @example Remove the like for the currently authenticated user on the media item with the ID of 777 - # Instagram.unlike_media(777) - # @format :json - # @authenticated true - # @rate_limited true - # @see http://instagram.com/developer/endpoints/likes/#delete_likes - def unlike_media(id, options={}) - response = delete("media/#{id}/likes", options, signature=true) - response - end - end - end -end diff --git a/lib/instagram/client/locations.rb b/lib/instagram/client/locations.rb deleted file mode 100644 index 0de44db..0000000 --- a/lib/instagram/client/locations.rb +++ /dev/null @@ -1,75 +0,0 @@ -module Instagram - class Client - # Defines methods related to media items - module Locations - # Returns extended information of a given Instagram location - # - # @overload location(id) - # @param location [Integer] An Instagram location ID - # @return [Hashie::Mash] The requested location. - # @example Return extended information for the Instagram office - # Instagram.location(514276) - # @format :json - # @authenticated false - # @rate_limited true - # @see http://instagram.com/developer/endpoints/locations/#get_locations - def location(id, *args) - response = get("locations/#{id}") - response - end - - # Returns a list of recent media items for a given Instagram location - # - # @overload location_recent_media(id, options={}) - # @param user [Integer] An Instagram location ID. - # @param options [Hash] A customizable set of options. - # @option options [Integer] :max_timestamp (nil) Return media before this UNIX timestamp - # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. - # @option options [Integer] :count (nil) Limits the number of results returned per page. - # @return [Hashie::Mash] - # @example Return a list of the most recent media items taken at the Instagram office - # Instagram.location_recent_media(514276) - # @see http://instagram.com/developer/endpoints/locations/#get_locations_media_recent - # @format :json - # @authenticated false - # @rate_limited true - def location_recent_media(id, *args) - options = args.last.is_a?(Hash) ? args.pop : {} - response = get("locations/#{id}/media/recent", options) - response - end - - # Returns Instagram locations within proximity of given lat,lng or foursquare venue id - # - # @overload location_search(options={}) - # @param foursquare_v2_id [String] A valid Foursquare Venue ID (v2) - # @param lat [String] A given latitude in decimal format - # @param lng [String] A given longitude in decimal format - # @option options [Integer] :count The number of media items to retrieve. - # @return [Hashie::Mash] location resultm object, #data is an Array. - # @example 1: Return a location with the Foursquare Venue ID = () - # Instagram.location_search("3fd66200f964a520c5f11ee3") (Schiller's Liquor Bar, 131 Rivington St., NY, NY 10002) - # @example 2: Return locations around 37.7808851, -122.3948632 (164 S Park, SF, CA USA) - # Instagram.location_search("37.7808851", "-122.3948632") - # @see http://instagram.com/developer/endpoints/locations/#get_locations_search - # @format :json - # @authenticated false - # @rate_limited true - def location_search(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - case args.size - when 1 - foursquare_v2_id = args.first - response = get('locations/search', options.merge(:foursquare_v2_id => foursquare_v2_id)) - when 2 - lat, lng = args - response = get('locations/search', options.merge(:lat => lat, :lng => lng)) - when 3 - lat, lng, distance = args - response = get('locations/search', options.merge(:lat => lat, :lng => lng, :distance => distance)) - end - response - end - end - end -end diff --git a/lib/instagram/client/media.rb b/lib/instagram/client/media.rb index 5df2c62..b1bebd4 100644 --- a/lib/instagram/client/media.rb +++ b/lib/instagram/client/media.rb @@ -3,80 +3,7 @@ class Client # Defines methods related to media items module Media # Returns extended information of a given media item - # - # @overload media_item(id) - # @param user [Integer] An Instagram media item ID - # @return [Hashie::Mash] The requested media item. - # @example Return extended information for media item 1234 - # Instagram.media_item(1324) - # @format :json - # @authenticated false unless requesting media from a protected user - # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/media/#get_media - def media_item(*args) - id = args.first || 'self' - response = get("media/#{id}") - response - end - - # Returns extended information of a given media item - # - # @overload media_shortcode(shortcode) - # @param shortcode [String] An Instagram media item shortcode - # @return [Hashie::Mash] The requested media item. - # @example Return extended information for media item with shortcode 'D' - # Instagram.media_shortcode('D') - # @format none - # @authenticated false unless requesting media from a protected user - # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/media/#get_media_by_shortcode - def media_shortcode(*args) - shortcode = args.first - response = get("media/shortcode/#{shortcode}", {}, false, false, true) - response - end - - # Returns a list of the overall most popular media - # - # @overload media_popular(options={}) - # @param options [Hash] A customizable set of options. - # @return [Hashie::Mash] - # @example Returns a list of the overall most popular media - # Instagram.media_popular - # @see http://instagram.com/developer/endpoints/media/#get_media_popular - # @format :json - # @authenticated false unless requesting it from a protected user - # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - def media_popular(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - id = args.first || "self" - response = get("media/popular", options) - response - end - - # Returns media items within proximity of given lat,lng - # - # @param lat [String] A given latitude in decimal format - # @param lng [String] A given longitude in decimal format - # @param options [Hash] A customizable set of options. - # @option options [Integer] :count The number of media items to retrieve. - # @return [Hashie::Mash] A list of matching media - # @example Return media around 37.7808851, -122.3948632 (164 S Park, SF, CA USA) - # Instagram.media_search("37.7808851", "-122.3948632") - # @see http://instagram.com/developer/endpoints/media/#get_media_search - # @format :json - # @authenticated false - # @rate_limited true - def media_search(lat, lng, options={}) - response = get('media/search', options.merge(:lat => lat, :lng => lng)) - response - end + # For implementation please refer to: https://developers.facebook.com/docs/instagram-basic-display-api/reference/media end end end diff --git a/lib/instagram/client/subscriptions.rb b/lib/instagram/client/subscriptions.rb deleted file mode 100644 index 8766689..0000000 --- a/lib/instagram/client/subscriptions.rb +++ /dev/null @@ -1,211 +0,0 @@ -require 'openssl' -require 'multi_json' - -module Instagram - class Client - # Defines methods related to real-time - module Subscriptions - # Returns a list of active real-time subscriptions - # - # @overload subscriptions(options={}) - # @return [Hashie::Mash] The list of subscriptions. - # @example Returns a list of subscriptions for the authenticated application - # Instagram.subscriptions - # @format :json - # @authenticated true - # - # Requires client_secret to be set on the client or passed in options - # @rate_limited true - # @see https://api.instagram.com/developer/realtime/ - def subscriptions(options={}) - response = get("subscriptions", options.merge(:client_secret => client_secret)) - response - end - - # Creates a real-time subscription - # - # @overload create_subscription(options={}) - # @param options [Hash] A set of parameters - # @option options [String] :object The object you'd like to subscribe to (user, tag, location or geography) - # @option options [String] :callback_url The subscription callback URL - # @option options [String] :aspect The aspect of the object you'd like to subscribe to (in this case, "media"). - # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively - # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object - # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object - # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point - # @overload create_subscription(object, callback_url, aspect="media", options={}) - # @param object [String] The object you'd like to subscribe to (user, tag, location or geography) - # @param callback_url [String] The subscription callback URL - # @param aspect [String] he aspect of the object you'd like to subscribe to (in this case, "media"). - # @param options [Hash] Addition options and parameters - # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively - # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object - # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object - # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point - # - # Note that we only support "media" at this time, but we might support other types of subscriptions in the future. - # @return [Hashie::Mash] The subscription created. - # @example Creates a new subscription to receive notifications for user media changes. - # Instagram.create_subscription("user", "http://example.com/instagram/callback") - # @format :json - # @authenticated true - # - # Requires client_secret to be set on the client or passed in options - # @rate_limited true - # @see https://api.instagram.com/developer/realtime/ - def create_subscription(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - object = args.shift - callback_url = args.shift - aspect = args.shift - options.tap {|o| - o[:object] = object unless object.nil? - o[:callback_url] = callback_url unless callback_url.nil? - o[:aspect] = aspect || o[:aspect] || "media" - } - response = post("subscriptions", options.merge(:client_secret => client_secret), signature=true) - response - end - - # Deletes a real-time subscription - # - # @overload delete_subscription(options={}) - # @param options [Hash] Addition options and parameters - # @option options [Integer] :subscription_id The subscription's ID - # @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) - # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id - # @overload delete_subscription(subscription_id, options={}) - # @param subscription_id [Integer] The subscription's ID - # @param options [Hash] Addition options and parameters - # @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) - # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id - # @return [Hashie::Mash] - # @example Deletes an application's user change subscription - # Instagram.delete_subscription(:object => "user") - # @format :json - # @authenticated true - # - # Requires client_secret to be set on the client or passed in options - # @rate_limited true - # @see https://api.instagram.com/developer/realtime/ - def delete_subscription(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - subscription_id = args.first - options.merge!(:id => subscription_id) if subscription_id - response = delete("subscriptions", options.merge(:client_secret => client_secret), signature=true) - response - end - - # As a security measure (to prevent DDoS attacks), Instagram sends a verification request to your server - # after you request a subscription. - # This method parses the challenge params and makes sure the call is legitimate. - # - # @param params the request parameters sent by Instagram. (You can pass in a Rails params hash.) - # @param verify_token the verify token sent in the {#subscribe subscription request}, if you provided one - # - # @yield verify_token if you need to compute the verification token - # (for instance, if your callback URL includes a record ID, which you look up - # and use to calculate a hash), you can pass meet_challenge a block, which - # will receive the verify_token received back from Instagram. - # - # @return the challenge string to be sent back to Instagram, or false if the request is invalid. - def meet_challenge(params, verify_token = nil, &verification_block) - if params["hub.mode"] == "subscribe" && - # you can make sure this is legitimate through two ways - # if your store the token across the calls, you can pass in the token value - # and we'll make sure it matches - ((verify_token && params["hub.verify_token"] == verify_token) || - # alternately, if you sent a specially-constructed value (such as a hash of various secret values) - # you can pass in a block, which we'll call with the verify_token sent by Instagram - # if it's legit, return anything that evaluates to true; otherwise, return nil or false - (verification_block && yield(params["hub.verify_token"]))) - params["hub.challenge"] - else - false - end - end - - # Public: As a security measure, all updates from Instagram are signed using - # X-Hub-Signature: XXXX where XXX is the sha1 of the json payload - # using your application secret as the key. - # - # Example: - # # in Rails controller - # def receive_update - # if Instagram.validate_update(request.body, headers) - # ... - # else - # render text: "not authorized", status: 401 - # end - # end - def validate_update(body, headers) - unless client_secret - raise ArgumentError, "client_secret must be set during configure" - end - - if request_signature = headers['X-Hub-Signature'] || headers['HTTP_X_HUB_SIGNATURE'] - calculated_signature = OpenSSL::HMAC.hexdigest('sha1', client_secret, body) - calculated_signature == request_signature - end - end - - # Process a subscription notification JSON payload - # - # @overload process_subscription(json, &block) - # @param json [String] The JSON response received by the Instagram real-time server - # @param block [Proc] A callable in which callbacks are defined - # @option options [String] :signature Pass in an X-Hub-Signature to use for payload validation - # @return [nil] - # @example Process and handle a notification for a user media change - # Instagram.process_subscription(params[:body]) do |handler| - # - # handler.on_user_changed do |user_id, data| - # - # user = User.by_instagram_id(user_id) - # @client = Instagram.client(:access_token => _access_token_for_user(user)) - # latest_media = @client.user_recent_media[0] - # user.media.create_with_hash(latest_media) - # end - # - # end - # @format :json - # @authenticated true - # - # Requires client_secret to be set on the client or passed in options - # @rate_limited true - # @see https://api.instagram.com/developer/realtime/ - def process_subscription(json, options={}, &block) - raise ArgumentError, "callbacks block expected" unless block_given? - - if options.has_key?(:signature) - if !client_secret - raise ArgumentError, "client_secret must be set during configure" - end - digest = OpenSSL::Digest.new('sha1') - verify_signature = OpenSSL::HMAC.hexdigest(digest, client_secret, json) - - if options[:signature] != verify_signature - raise Instagram::InvalidSignature, "invalid X-Hub-Signature does not match verify signature against client_secret" - end - end - - payload = MultiJson.decode(json) - @changes = Hash.new { |h,k| h[k] = [] } - for change in payload - @changes[change['object']] << change - end - block.call(self) - end - - [:user, :tag, :location, :geography].each do |object| - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ +1 - def on_#{object}_changed(&block) - for change in @changes['#{object}'] - yield change.delete('object_id'), change - end - end - RUBY_EVAL - end - end - end -end diff --git a/lib/instagram/client/tags.rb b/lib/instagram/client/tags.rb deleted file mode 100644 index 4e3b8fa..0000000 --- a/lib/instagram/client/tags.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Instagram - class Client - # Defines methods related to tags - module Tags - # Returns extended information of a given Instagram tag - # - # @overload tag(tag) - # @param tag [String] An Instagram tag name - # @return [Hashie::Mash] The requested tag. - # @example Return extended information for the tag "cat" - # Instagram.tag('cat') - # @format :json - # @authenticated false - # @rate_limited true - # @see http://instagram.com/developer/endpoints/tags/#get_tags - def tag(tag, *args) - response = get("tags/#{tag}") - response - end - - # Returns a list of recent media items for a given Instagram tag - # - # @overload tag_recent_media(tag, options={}) - # @param tag-name [String] An Instagram tag name. - # @param options [Hash] A customizable set of options. - # @option options [Integer] :max_tag_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. The value can be retrieved from the returned response via pagination.max_tag_id. - # @option options [Integer] :min_tag_id (nil) Returns results with an ID greater than (that is, newer than) or equal to the specified ID. The value can be retrieved from the returned response via pagination.min_tag_id. - # @return [Hashie::Mash] - # @example Return a list of the most recent media items tagged "cat" - # Instagram.tag_recent_media('cat') - # @see https://instagram.com/developer/endpoints/tags/#get_tags_media_recent - # @format :json - # @authenticated false - # @rate_limited true - def tag_recent_media(id, *args) - options = args.last.is_a?(Hash) ? args.pop : {} - response = get("tags/#{id}/media/recent", options, false, false, false) - response - end - - # Returns a list of tags starting with the given search query - # - # @format :json - # @authenticated false - # @rate_limited true - # @param query [String] The beginning or complete tag name to search for - # @param options [Hash] A customizable set of options. - # @option options [Integer] :count The number of media items to retrieve. - # @return [Hashie::Mash] - # @see http://instagram.com/developer/endpoints/tags/#get_tags_search - # @example Return tags that start with "cat" - # Instagram.tag_search("cat") - def tag_search(query, options={}) - response = get('tags/search', options.merge(:q => query)) - response - end - end - end -end diff --git a/lib/instagram/client/users.rb b/lib/instagram/client/users.rb index 2c6488e..f25451a 100644 --- a/lib/instagram/client/users.rb +++ b/lib/instagram/client/users.rb @@ -3,308 +3,32 @@ class Client # Defines methods related to users module Users # Returns extended information of a given user - # - # @overload user(id=nil, options={}) - # @param user [Integer] An Instagram user ID - # @return [Hashie::Mash] The requested user. - # @example Return extended information for @shayne - # Instagram.user(20) # @format :json - # @authenticated false unless requesting it from a protected user # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - # @see http://instagram.com/developer/endpoints/users/#get_users + # @see https://developers.facebook.com/docs/instagram-basic-display-api/reference/user def user(*args) options = args.last.is_a?(Hash) ? args.pop : {} - id = args.first || 'self' - response = get("users/#{id}", options) + id = args.first || 'me' + response = graph_get("#{id}", options) response end - - # Returns users that match the given query - # - # @format :json - # @authenticated false - # @rate_limited true - # @param query [String] The search query to run against user search. - # @param options [Hash] A customizable set of options. - # @option options [Integer] :count The number of users to retrieve. - # @return [Hashie::Mash] - # @see http://instagram.com/developer/endpoints/users/#get_users_search - # @example Return users that match "Shayne Sweeney" - # Instagram.user_search("Shayne Sweeney") - def user_search(query, options={}) - response = get('users/search', options.merge(:q => query)) - response - end - - # Returns a list of users whom a given user follows - # - # @overload user_follows(id=nil, options={}) - # @param options [Hash] A customizable set of options. - # @return [Hashie::Mash] - # @example Returns a list of users the authenticated user follows - # Instagram.user_follows - # @overload user_follows(id=nil, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] A customizable set of options. - # @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. - # @option options [Integer] :count (nil) Limits the number of results returned per page. - # @return [Hashie::Mash] - # @example Return a list of users @mikeyk follows - # Instagram.user_follows(4) # @mikeyk user ID being 4 - # @see http://instagram.com/developer/endpoints/relationships/#get_users_follows - # @format :json - # @authenticated false unless requesting it from a protected user - # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - def user_follows(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - id = args.first || "self" - response = get("users/#{id}/follows", options) - response - end - end - - # Returns a list of users whom a given user is followed by - # - # @overload user_followed_by(id=nil, options={}) - # @param options [Hash] A customizable set of options. - # @return [Hashie::Mash] - # @example Returns a list of users the authenticated user is followed by - # Instagram.user_followed_by - # @overload user_followed_by(id=nil, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] A customizable set of options. - # @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. - # @option options [Integer] :count (nil) Limits the number of results returned per page. - # @return [Hashie::Mash] - # @example Return a list of users @mikeyk is followed by - # Instagram.user_followed_by(4) # @mikeyk user ID being 4 - # @see http://instagram.com/developer/endpoints/relationships/#get_users_followed_by - # @format :json - # @authenticated false unless requesting it from a protected user - # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true - def user_followed_by(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - id = args.first || "self" - response = get("users/#{id}/followed-by", options) - response end - # Returns a list of users who have requested the currently authorized user's permission to follow - # - # @overload user_requested_by() - # @param options [Hash] A customizable set of options. - # @return [Hashie::Mash] - # @example Returns a list of users awaiting approval of a ollow request, for the authenticated user - # Instagram.user_requested_by - # @overload user_requested_by() - # @return [Hashie::Mash] - # @example Return a list of users who have requested to follow the authenticated user - # Instagram.user_requested_by() - # @see http://instagram.com/developer/endpoints/relationships/#get_incoming_requests - # @format :json - # @authenticated true - # @rate_limited true - def user_requested_by() - response = get("users/self/requested-by") - response - end - - # Returns most recent media items from the currently authorized user's feed - # - # @overload user_media_feed(options={}) - # @param options [Hash] A customizable set of options. - # @option options [Integer] :max_id Returns results with an ID less than (that is, older than) or equal to the specified ID. - # @option options [Integer] :min_id Return media later than this min_id - # @option options [Integer] :count Specifies the number of records to retrieve, per page. - # @return [Hashie::Mash] - # @example Return most recent media images that would appear on @shayne's feed - # Instagram.user_media_feed() # assuming @shayne is the authorized user - # @format :json - # @authenticated true - # @rate_limited true - # @see http://instagram.com/developer/endpoints/users/#get_users_feed - def user_media_feed(*args) - options = args.first.is_a?(Hash) ? args.pop : {} - response = get('users/self/feed', options) - response - end # Returns a list of recent media items for a given user - # - # @overload user_recent_media(options={}) - # @param options [Hash] A customizable set of options. - # @return [Hashie::Mash] - # @example Returns a list of recent media items for the currently authenticated user - # Instagram.user_recent_media - # @overload user_recent_media(id=nil, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] A customizable set of options. - # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. - # @option options [Integer] :count (nil) Limits the number of results returned per page. - # @return [Hashie::Mash] - # @example Return a list of media items taken by @mikeyk - # Instagram.user_recent_media(4) # @mikeyk user ID being 4 - # @see http://instagram.com/developer/endpoints/users/#get_users_media_recent # @format :json - # @authenticated false unless requesting it from a protected user # - # If getting this data of a protected user, you must authenticate (and be allowed to see that user). - # @rate_limited true + # @see https://developers.facebook.com/docs/instagram-basic-display-api/reference/user/media def user_recent_media(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - id = args.first || "self" - response = get("users/#{id}/media/recent", options) - response - end - - # Returns a list of media items liked by the current user - # - # @overload user_liked_media(options={}) - # @param options [Hash] A customizable set of options. - # @option options [Integer] :max_like_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID. - # @option options [Integer] :count (nil) Limits the number of results returned per page. - # @return [Hashie::Mash] - # @example Returns a list of media items liked by the currently authenticated user - # Instagram.user_liked_media - # @see http://instagram.com/developer/endpoints/users/#get_users_liked_feed - # @format :json - # @authenticated true - # @rate_limited true - def user_liked_media(options={}) - response = get("users/self/media/liked", options) + options = args.last.is_a?(Hash) ? args.pop.select{|k,v| v.present?} : {} + options[:fields] = media_query_fields + id = args.first || "me" + response = graph_get("#{id}/media", options) response end - # Returns information about the current user's relationship (follow/following/etc) to another user - # - # @overload user_relationship(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Return the relationship status between the currently authenticated user and @mikeyk - # Instagram.user_relationship(4) # @mikeyk user ID being 4 - # @see http://instagram.com/developer/endpoints/relationships/#get_relationship - # @format :json - # @authenticated true - # @rate_limited true - def user_relationship(id, options={}) - response = get("users/#{id}/relationship", options) - response - end - - # Create a follows relationship between the current user and the target user - # - # @overload follow_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Request the current user to follow the target user - # Instagram.follow_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def follow_user(id, options={}) - options["action"] = "follow" - response = post("users/#{id}/relationship", options, signature=true) - response - end - - # Destroy a follows relationship between the current user and the target user - # - # @overload unfollow_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Remove a follows relationship between the current user and the target user - # Instagram.unfollow_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def unfollow_user(id, options={}) - options["action"] = "unfollow" - response = post("users/#{id}/relationship", options, signature=true) - response - end - - # Block a relationship between the current user and the target user - # - # @overload unfollow_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Block a relationship between the current user and the target user - # Instagram.block_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def block_user(id, options={}) - options["action"] = "block" - response = post("users/#{id}/relationship", options, signature=true) - response - end - - # Remove a relationship block between the current user and the target user - # - # @overload unblock_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Remove a relationship block between the current user and the target user - # Instagram.unblock_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def unblock_user(id, options={}) - options["action"] = "unblock" - response = post("users/#{id}/relationship", options, signature=true) - response - end - - # Approve a relationship request between the current user and the target user - # - # @overload approve_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Approve a relationship request between the current user and the target user - # Instagram.approve_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def approve_user(id, options={}) - options["action"] = "approve" - response = post("users/#{id}/relationship", options, signature=true) - response - end - - # Deny a relationship request between the current user and the target user - # - # @overload deny_user(id, options={}) - # @param user [Integer] An Instagram user ID. - # @param options [Hash] An optional options hash - # @return [Hashie::Mash] - # @example Deny a relationship request between the current user and the target user - # Instagram.deny_user(4) - # @see http://instagram.com/developer/endpoints/relationships/#post_relationship - # @format :json - # @authenticated true - # @rate_limited true - def deny_user(id, options={}) - options["action"] = "deny" - response = post("users/#{id}/relationship", options, signature=true) - response + def media_query_fields + 'id,username,caption,media_type,media_url,permalink,thumbnail_url,timestamp' end end end diff --git a/lib/instagram/configuration.rb b/lib/instagram/configuration.rb index 72e03a3..5025ee3 100644 --- a/lib/instagram/configuration.rb +++ b/lib/instagram/configuration.rb @@ -15,6 +15,8 @@ module Configuration :scope, :redirect_uri, :endpoint, + :token_endpoint, + :graph_api_endpoint, :format, :proxy, :user_agent, @@ -48,6 +50,10 @@ module Configuration # @note There is no reason to use any other endpoint at this time DEFAULT_ENDPOINT = 'https://api.instagram.com/v1/'.freeze + # Use graph end point for the new instagram basic display api + TOKEN_ENDPOINT = 'https://api.instagram.com/'.freeze + GRAPH_API_ENDPOINT = 'https://graph.instagram.com/'.freeze + # The response format appended to the path and sent in the 'Accept' header if none is set # # @note JSON is the only available format at this time @@ -111,6 +117,8 @@ def reset self.scope = DEFAULT_SCOPE self.redirect_uri = DEFAULT_REDIRECT_URI self.endpoint = DEFAULT_ENDPOINT + self.token_endpoint = TOKEN_ENDPOINT + self.graph_api_endpoint = GRAPH_API_ENDPOINT self.format = DEFAULT_FORMAT self.proxy = DEFAULT_PROXY self.user_agent = DEFAULT_USER_AGENT diff --git a/lib/instagram/connection.rb b/lib/instagram/connection.rb index 668718e..ee56e55 100644 --- a/lib/instagram/connection.rb +++ b/lib/instagram/connection.rb @@ -6,11 +6,13 @@ module Instagram module Connection private - def connection(raw=false) + def connection(raw=false, graph_request=false) + end_point = graph_request ? graph_api_endpoint : token_endpoint + options = { :headers => {'Accept' => "application/#{format}; charset=utf-8", 'User-Agent' => user_agent}, :proxy => proxy, - :url => endpoint, + :url => end_point, }.merge(connection_options) Faraday::Connection.new(options) do |connection| diff --git a/lib/instagram/error.rb b/lib/instagram/error.rb index a2a277e..0009979 100644 --- a/lib/instagram/error.rb +++ b/lib/instagram/error.rb @@ -5,6 +5,8 @@ class Error < StandardError; end # Raised when Instagram returns the HTTP status code 400 class BadRequest < Error; end + class AccessTokenExpired < Error; end + # Raised when Instagram returns the HTTP status code 404 class NotFound < Error; end diff --git a/lib/instagram/oauth.rb b/lib/instagram/oauth.rb index 9a86f59..fdf4ed2 100644 --- a/lib/instagram/oauth.rb +++ b/lib/instagram/oauth.rb @@ -12,10 +12,35 @@ def authorize_url(options={}) # Return an access token from authorization def get_access_token(code, options={}) + short_lived_auth = get_short_lived_access_token(code, options) + res = get_long_lived_access_token(short_lived_auth.access_token) + res[:user_id] = short_lived_auth[:user_id] + + temp_instagram = Instagram.client(:access_token => res.access_token) + res[:user] = temp_instagram.user(fields: 'id,username') + res + end + + def get_short_lived_access_token(code, options={}) options[:grant_type] ||= "authorization_code" options[:redirect_uri] ||= self.redirect_uri params = access_token_params.merge(options) - post("/oauth/access_token/", params.merge(:code => code), signature=false, raw=false, unformatted=true, no_response_wrapper=true) + + post("/oauth/access_token/", params.merge(:code => code), unformatted: true, no_response_wrapper: true) + end + + # Returns an access token that will expire for about 60 days + def get_long_lived_access_token(short_live_access_token, options={}) + options[:grant_type] = 'ig_exchange_token' + options[:client_secret] = self.client_secret + options[:access_token] = short_live_access_token + get("/access_token/", options, unformatted: true, no_response_wrapper: true, graph_request: true) + end + + def refresh_instagram_token(long_live_access_token, options={}) + options[:grant_type] = 'ig_refresh_token' + options[:access_token] = long_live_access_token + get("/refresh_access_token/", options, unformatted: true, no_response_wrapper: true, graph_request: true) end private diff --git a/lib/instagram/request.rb b/lib/instagram/request.rb index 9c5f061..1296cf7 100644 --- a/lib/instagram/request.rb +++ b/lib/instagram/request.rb @@ -5,30 +5,34 @@ module Instagram # Defines HTTP request methods module Request # Perform an HTTP GET request - def get(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests) - request(:get, path, options, signature, raw, unformatted, no_response_wrapper, signed) + def get(path, options={}, signature: false, raw: false, unformatted: false, no_response_wrapper: no_response_wrapper(), signed: sign_requests, graph_request: false) + request(:get, path, options, signature, raw, unformatted, no_response_wrapper, signed, graph_request) end # Perform an HTTP POST request - def post(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests) - request(:post, path, options, signature, raw, unformatted, no_response_wrapper, signed) + def post(path, options={}, signature: false, raw: false, unformatted: false, no_response_wrapper: no_response_wrapper(), signed: sign_requests, graph_request: false) + request(:post, path, options, signature, raw, unformatted, no_response_wrapper, signed, graph_request) end # Perform an HTTP PUT request - def put(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests) - request(:put, path, options, signature, raw, unformatted, no_response_wrapper, signed) + def put(path, options={}, signature: false, raw: false, unformatted: false, no_response_wrapper: no_response_wrapper(), signed: sign_requests, graph_request: false) + request(:put, path, options, signature, raw, unformatted, no_response_wrapper, signed, graph_request) end # Perform an HTTP DELETE request - def delete(path, options={}, signature=false, raw=false, unformatted=false, no_response_wrapper=no_response_wrapper(), signed=sign_requests) - request(:delete, path, options, signature, raw, unformatted, no_response_wrapper, signed) + def delete(path, options={}, signature: false, raw: false, unformatted: false, no_response_wrapper: no_response_wrapper(), signed: sign_requests, graph_request: false) + request(:delete, path, options, signature, raw, unformatted, no_response_wrapper, signed, graph_request) + end + + def graph_get(path, options={}) + get(path, options, unformatted: true, no_response_wrapper: true, graph_request: true) end private # Perform an HTTP request - def request(method, path, options, signature=false, raw=false, unformatted=false, no_response_wrapper=false, signed=sign_requests) - response = connection(raw).send(method) do |request| + def request(method, path, options, signature=false, raw=false, unformatted=false, no_response_wrapper=false, signed=sign_requests, graph_request=false) + response = connection(raw, graph_request).send(method) do |request| path = formatted_path(path) unless unformatted if signed == true diff --git a/lib/instagram/version.rb b/lib/instagram/version.rb index 6f4a234..0748a0e 100644 --- a/lib/instagram/version.rb +++ b/lib/instagram/version.rb @@ -1,3 +1,3 @@ module Instagram - VERSION = '1.1.6'.freeze unless defined?(::Instagram::VERSION) + VERSION = '2.0.10'.freeze unless defined?(::Instagram::VERSION) end