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] The 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 do |o| o[:object] = object unless object.nil? o[:callback_url] = callback_url unless callback_url.nil? o[:aspect] = aspect || o[:aspect] || "media" end 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, include 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, include 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[: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 = {}) raise ArgumentError, "callbacks block expected" unless block_given? if options.key?(:signature) unless 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 yield(self) end %i[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