# graph_batch_api and legacy are required at the bottom, since they depend on API being defined require 'koala/api/graph_api_methods' require 'koala/api/graph_collection' require 'openssl' module Koala module Facebook class API # Creates a new API client. # @param [String] access_token access token # @param [String] app_secret app secret, for tying your access tokens to your app secret # If you provide an app secret, your requests will be # signed by default, unless you pass appsecret_proof: # false as an option to the API call. (See # https://developers.facebook.com/docs/graph-api/securing-requests/) # @param [Block] rate_limit_hook block called with limits received in facebook response headers # @note If no access token is provided, you can only access some public information. # @return [Koala::Facebook::API] the API client def initialize(access_token = Koala.config.access_token, app_secret = Koala.config.app_secret, rate_limit_hook = Koala.config.rate_limit_hook) @access_token = access_token @app_secret = app_secret @rate_limit_hook = rate_limit_hook end attr_reader :access_token, :app_secret, :rate_limit_hook include GraphAPIMethods # Make a call directly to the Graph API. # (See any of the other methods for example invocations.) # # @param path the Graph API path to query (no leading / needed) # @param args (see #get_object) # @param verb the type of HTTP request to make (get, post, delete, etc.) # @options (see #get_object) # # @yield response when making a batch API call, you can pass in a block # that parses the results, allowing for cleaner code. # The block's return value is returned in the batch results. # See the code for {#get_picture} for examples. # (Not needed in regular calls; you'll probably rarely use this.) # # @raise [Koala::Facebook::APIError] if Facebook returns an error # # @return the result from Facebook def graph_call(path, args = {}, verb = "get", options = {}, &post_processing) # enable appsecret_proof by default options = {:appsecret_proof => true}.merge(options) if @app_secret response = api(path, args, verb, options) error = GraphErrorChecker.new(response.status, response.body, response.headers).error_if_appropriate raise error if error # if we want a component other than the body (e.g. redirect header for images), provide that http_component = options[:http_component] desired_data = if options[:http_component] http_component == :response ? response : response.send(http_component) else # turn this into a GraphCollection if it's pageable API::GraphCollection.evaluate(response, self) end if rate_limit_hook limits = %w(x-business-use-case-usage x-ad-account-usage x-app-usage).each_with_object({}) do |key, hash| value = response.headers.fetch(key, nil) next unless value hash[key] = JSON.parse(response.headers[key]) rescue JSON::ParserError => e Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{key} = #{value}") end rate_limit_hook.call(limits) if limits.keys.any? end # now process as appropriate for the given call (get picture header, etc.) post_processing ? post_processing.call(desired_data) : desired_data end # Makes a request to the appropriate Facebook API. # @note You'll rarely need to call this method directly. # # @see GraphAPIMethods#graph_call # # @param path the server path for this request (leading / is prepended if not present) # @param args arguments to be sent to Facebook # @param verb the HTTP method to use # @param options request-related options for Koala and Faraday. # See https://github.com/arsduo/koala/wiki/HTTP-Services for additional options. # @option options [Symbol] :http_component which part of the response (headers, body, or status) to return # @option options [Symbol] :format which request format to use. Currently, :json is supported # @option options [Symbol] :preserve_form_arguments preserve arrays in arguments, which are # expected by certain FB APIs (see the ads API in particular, # https://developers.facebook.com/docs/marketing-api/adgroup/v2.4) # @option options [Boolean] :beta use Facebook's beta tier # @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless. # (All API requests with access tokens use SSL.) # @raise [Koala::Facebook::ServerError] if Facebook returns an error (response status >= 500) # # @return a Koala::HTTPService::Response object representing the returned Facebook data def api(path, args = {}, verb = "get", options = {}) # we make a copy of args so the modifications (added access_token & appsecret_proof) # do not affect the received argument args = args.dup # If a access token is explicitly provided, use that # This is explicitly needed in batch requests so GraphCollection # results preserve any specific access tokens provided args["access_token"] ||= @access_token || @app_access_token if @access_token || @app_access_token if options.delete(:appsecret_proof) && args["access_token"] && @app_secret args["appsecret_proof"] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @app_secret, args["access_token"]) end # Translate any arrays in the params into comma-separated strings args = sanitize_request_parameters(args) unless preserve_form_arguments?(options) # add a leading / if needed... path = "/#{path}" unless path.to_s =~ /^\// # make the request via the provided service result = Koala.make_request(path, args, verb, options) if result.status.to_i >= 500 raise Koala::Facebook::ServerError.new(result.status.to_i, result.body) end result end private # Sanitizes Ruby objects into Facebook-compatible string values. # # @param parameters a hash of parameters. # # Returns a hash in which values that are arrays of non-enumerable values # (Strings, Symbols, Numbers, etc.) are turned into comma-separated strings. def sanitize_request_parameters(parameters) parameters.reduce({}) do |result, (key, value)| # if the parameter is an array that contains non-enumerable values, # turn it into a comma-separated list # in Ruby 1.8.7, strings are enumerable, but we don't care if value.is_a?(Array) && value.none? {|entry| entry.is_a?(Enumerable) && !entry.is_a?(String)} value = value.join(",") end result.merge(key => value) end end def preserve_form_arguments?(options) options[:format] == :json || options[:preserve_form_arguments] || Koala.config.preserve_form_arguments end def check_response(http_status, body, headers) end end end end