require 'openssl' require 'base64' require 'httparty' require 'cgi' module Bluekai # A simple BlueKai client class Client attr_reader :domain, :api_user_key def initialize(opts = {}) @api_user_key = opts.fetch(:api_user_key, ENV['BLUEKAI_API_USER_KEY']) || fail(Error, 'BlueKai API user key missing') @api_private_key = opts.fetch(:api_private_key, ENV['BLUEKAI_API_PRIVATE_KEY']) || fail(Error, 'BlueKai API private key missing') @partner_id = opts.fetch(:partner_id, ENV['BLUEKAI_PARTNER_ID']) || fail(Error, 'BlueKai PartnerID missing') @opts = opts end def ping request('GET', '/Services/WS/Ping', {}).to_i == 200 rescue false end # Public: Lists categories in the BlueKai taxonomy. API definition # can be found here https://kb.bluekai.com/display/PD/Taxonomy+API # # parentId - integer # fullPath - {0,1} # bkSize - {0,1} Enter 1 to include the inventory of unique users in # the BlueKai network for each category. # intlDataCountryId - {-1..24} for country index see # (https://kb.bluekai.com/display/PD/Taxonomy+API) # device_code - {0 = Desktop + Mobile,1 = Desktop, 2 = Mobile} # showBuyable - {0,1} # showLeafStatus - {0,1} # description - {0,1} # vertical - {0,1} # showReceivedAudienceCategories - {0,1} # showCategoryPriceAtDate - {'YYYY-MM-DD'} # # Returns array of taxonomy nodes. def taxonomy(query = {}) request('GET', '/Services/WS/Taxonomy', query)[:nodeList] end private def request(method, path, query, body = nil) method.upcase! signature = sign(method, path, query_values(query), body_sorted(body)) url = "#{domain}#{path}?#{query_url_formatted(query)}"\ "bkuid=#{api_user_key}&bksig=#{signature}" response = case method when 'GET' HTTParty.get(url) when 'POST' HTTParty.post(url, body: body_sorted(body), headers: json_headers) when 'PUT' HTTParty.put(url, body: body_sorted(body), headers: json_headers) else fail ArgumentError, "request method '#{method}' not supported" end.response fail "HTTP Request Error: #{response.body}" if response.code != '200' return response.code if response.body == '' || response.body.nil? JSON.parse(response.body, symbolize_names: true) end def domain 'https://services.bluekai.com' end def body_sorted(body) # sort hash according to keys for correct signature computation body.sort_by { |key, _value| key.to_s }.to_h.to_json if body end def json_headers { 'Accept' => 'application/json', 'Content-type' => 'application/json' } end def query_values(query) return nil unless query query.values.join end def query_url_formatted(query) return nil unless query query.map { |k, v| "#{k}=#{v}" }.join('&') + '&' end # HMAC-SHA256(Secret key, HTTP_METHOD + URI_PATH + QUERY_ARG_VALUES + POST_DATA) def sign(method, path, query, body) string_to_sign = method + path + query.to_s + body.to_s digest = OpenSSL::Digest.new('sha256') hmac = OpenSSL::HMAC.digest(digest, @api_private_key, string_to_sign) CGI.escape(Base64.strict_encode64(hmac)) end end end