lib/sailthru.rb in sailthru-client-3.0.0 vs lib/sailthru.rb in sailthru-client-4.0.0

- old
+ new

@@ -1,973 +1,31 @@ -require 'rubygems' -require 'net/https' -require 'net/http' -require 'uri' -require 'cgi' -require 'json' -require 'digest/md5' -require 'net/http/post/multipart' +require 'sailthru/version' +require 'sailthru/client' module Sailthru + class ClientError < StandardError + end - Version = VERSION = '3.0.0' - - class SailthruClientException < Exception + class UnavailableError < StandardError end - class SailthruUnavailableException < Exception + # Provides a global place to configure the credentials for an application. + # For instance, in your Rails app, create +config/initializers/sailthru.rb+ + # and place this line in it: + # + # Sailthru.credentials('apikey', 'secret') + # + # Now you can create a client instance easily via Sailthru::Client.new + # + def self.credentials(api_key, secret) + @api_key = api_key + @secret = secret end - module Helpers - # params: - # params, Hash - # returns: - # Array, values of each item in the Hash (and nested hashes) - # - # Extracts the values of a set of parameters, recursing into nested assoc arrays. - def extract_param_values(params) - values = [] - params.each do |k, v| - if v.class == Hash - values.concat extract_param_values(v) - elsif v.class == Array - temp_hash = Hash.new() - v.each_with_index do |v_,i_| - temp_hash[i_.to_s] = v_ - end - values.concat extract_param_values(temp_hash) - else - values.push v.to_s - end - end - return values - end - - # params: - # params, Hash - # secret, String - # returns: - # String - # - # Returns the unhashed signature string (secret + sorted list of param values) for an API call. - def get_signature_string(params, secret) - return secret + extract_param_values(params).sort.join("") - end - - - # params: - # params, Hash - # secret, String - # returns: - # String - # - # Returns an MD5 hash of the signature string for an API call. - def get_signature_hash(params, secret) - Digest::MD5.hexdigest(get_signature_string(params, secret)).to_s - end - - - # Flatten nested hash for GET / POST request. - def flatten_nested_hash(hash, brackets = true) - f = {} - hash.each do |key, value| - _key = brackets ? "[#{key}]" : key.to_s - if value.class == Hash - flatten_nested_hash(value).each do |k, v| - f["#{_key}#{k}"] = v - end - elsif value.class == Array - temp_hash = Hash.new() - value.each_with_index do |v, i| - temp_hash[i.to_s] = v - end - flatten_nested_hash(temp_hash).each do |k, v| - f["#{_key}#{k}"] = v - end - - else - f[_key] = value - end - end - return f - end - + def self.api_key + @api_key end - class SailthruClient - - include Helpers - - attr_accessor :verify_ssl - - # params: - # api_key, String - # secret, String - # api_uri, String - # - # Instantiate a new client; constructor optionally takes overrides for key/secret/uri and proxy server settings. - def initialize(api_key, secret, api_uri=nil, proxy_host=nil, proxy_port=nil, opts={}) - @api_key = api_key - @secret = secret - @api_uri = if api_uri.nil? then 'https://api.sailthru.com' else api_uri end - @proxy_host = proxy_host - @proxy_port = proxy_port - @verify_ssl = true - @opts = opts - end - - # params: - # template_name, String - # email, String - # replacements, Hash - # options, Hash - # replyto: override Reply-To header - # test: send as test email (subject line will be marked, will not count towards stats) - # schedule_time, Date - # returns: - # Hash, response data from server - # - # Send a transactional email, or schedule one for the near future - # http://docs.sailthru.com/api/send - def send(template_name, email, vars={}, options = {}, schedule_time = nil) - warn "[DEPRECATION] `send` is deprecated. Please use `send_email` instead." - send_email(template_name, email, vars, options, schedule_time) - end - - # params: - # template_name, String - # email, String - # vars, Hash - # options, Hash - # replyto: override Reply-To header - # test: send as test email (subject line will be marked, will not count towards stats) - # returns: - # Hash, response data from server - def send_email(template_name, email, vars={}, options = {}, schedule_time = nil, limit = {}) - post = {} - post[:template] = template_name - post[:email] = email - post[:vars] = vars if vars.length >= 1 - post[:options] = options if options.length >= 1 - post[:schedule_time] = schedule_time if !schedule_time.nil? - post[:limit] = limit if limit.length >= 1 - return self.api_post(:send, post) - end - - - def multi_send(template_name, emails, vars={}, options = {}, schedule_time = nil, evars = {}) - post = {} - post[:template] = template_name - post[:email] = emails - post[:vars] = vars if vars.length >= 1 - post[:options] = options if options.length >= 1 - post[:schedule_time] = schedule_time if !schedule_time.nil? - post[:evars] = evars if evars.length >= 1 - return self.api_post(:send, post) - end - - - # params: - # send_id, Fixnum - # returns: - # Hash, response data from server - # - # Get the status of a send. - def get_send(send_id) - self.api_get(:send, {:send_id => send_id.to_s}) - end - - - def cancel_send(send_id) - self.api_delete(:send, {:send_id => send_id.to_s}) - end - - # params: - # name, String - # list, String - # schedule_time, String - # from_name, String - # from_email, String - # subject, String - # content_html, String - # content_text, String - # options, Hash - # returns: - # Hash, response data from server - # - # Schedule a mass mail blast - def schedule_blast(name, list, schedule_time, from_name, from_email, subject, content_html, content_text, options = {}) - post = options ? options : {} - post[:name] = name - post[:list] = list - post[:schedule_time] = schedule_time - post[:from_name] = from_name - post[:from_email] = from_email - post[:subject] = subject - post[:content_html] = content_html - post[:content_text] = content_text - api_post(:blast, post) - end - - # Schedule a mass mail blast from template - def schedule_blast_from_template(template, list, schedule_time, options={}) - post = options ? options : {} - post[:copy_template] = template - post[:list] = list - post[:schedule_time] = schedule_time - api_post(:blast, post) - end - - # Schedule a mass mail blast from previous blast - def schedule_blast_from_blast(blast_id, schedule_time, options={}) - post = options ? options : {} - post[:copy_blast] = blast_id - #post[:name] = name - post[:schedule_time] = schedule_time - api_post(:blast, post) - end - - - # params - # blast_id, Fixnum | String - # name, String - # list, String - # schedule_time, String - # from_name, String - # from_email, String - # subject, String - # content_html, String - # content_text, String - # options, hash - # - # updates existing blast - def update_blast(blast_id, name = nil, list = nil, schedule_time = nil, from_name = nil, from_email = nil, subject = nil, content_html = nil, content_text = nil, options = {}) - data = options ? options : {} - data[:blast_id] = blast_id - if name != nil - data[:name] = name - end - if list != nil - data[:list] = list - end - if schedule_time != nil - data[:schedule_time] = schedule_time - end - if from_name != nil - data[:from_name] = from_name - end - if from_email != nil - data[:from_email] = from_email - end - if subject != nil - data[:subject] = subject - end - if content_html != nil - data[:content_html] = content_html - end - if content_text != nil - data[:content_text] = content_text - end - api_post(:blast, data) - end - - - # params: - # blast_id, Fixnum | String - # options, hash - # returns: - # Hash, response data from server - # - # Get information on a previously scheduled email blast - def get_blast(blast_id, options={}) - options[:blast_id] = blast_id.to_s - api_get(:blast, options) - end - - # params: - # blast_id, Fixnum | String - # - # Cancel a scheduled Blast - def cancel_blast(blast_id) - api_post(:blast, {:blast_id => blast_id, :schedule_time => ''}) - end - - # params: - # blast_id, Fixnum | String - # - # Delete a Blast - def delete_blast(blast_id) - api_delete(:blast, {:blast_id => blast_id}) - end - - # params: - # email, String - # returns: - # Hash, response data from server - # - # Return information about an email address, including replacement vars and lists. - def get_email(email) - api_get(:email, {:email => email}) - end - - # params: - # email, String - # vars, Hash - # lists, Hash mapping list name => 1 for subscribed, 0 for unsubscribed - # options, Hash mapping optional parameters - # returns: - # Hash, response data from server - # - # Set replacement vars and/or list subscriptions for an email address. - def set_email(email, vars = {}, lists = {}, templates = {}, options = {}) - data = options - data[:email] = email - data[:vars] = vars unless vars.empty? - data[:lists] = lists unless lists.empty? - data[:templates] = templates unless templates.empty? - self.api_post(:email, data) - end - - # params: - # new_email, String - # old_email, String - # options, Hash mapping optional parameters - # returns: - # Hash of response data. - # - # change a user's email address. - def change_email(new_email, old_email, options = {}) - data = options - data[:email] = new_email - data[:change_email] = old_email - self.api_post(:email, data) - end - - # params: - # template_name, String - # returns: - # Hash of response data. - # - # Get a template. - def get_template(template_name) - self.api_get(:template, {:template => template_name}) - end - - - # params: - # template_name, String - # template_fields, Hash - # returns: - # Hash containg response from the server. - # - # Save a template. - def save_template(template_name, template_fields) - data = template_fields - data[:template] = template_name - self.api_post(:template, data) - end - - # params: - # template_name, String - # returns: - # Hash of response data. - # - # Delete a template. - def delete_template(template_name) - self.api_delete(:template, {:template => template_name}) - end - - - # params: - # params, Hash - # request, String - # returns: - # TrueClass or FalseClass, Returns true if the incoming request is an authenticated verify post. - def receive_verify_post(params, request) - if request.post? - [:action, :email, :send_id, :sig].each { |key| return false unless params.has_key?(key) } - - return false unless params[:action] == :verify - - sig = params.delete(:sig) - sig = sig.delete_if {|key, value| key == :controller} - return false unless sig == get_signature_hash(params, @secret) - - _send = self.get_send(params[:send_id]) - return false unless _send.has_key?(:email) - - return false unless _send[:email] == params[:email] - - return true - else - return false - end - end - - # params: - # params, Hash - # request, String - # returns: - # TrueClass or FalseClass, Returns true if the incoming request is an authenticated optout post. - def receive_optout_post(params, request) - if request.post? - [:action, :email, :sig].each { |key| return false unless params.has_key?(key) } - - return false unless params[:action] == 'optout' - - sig = params.delete(:sig) - sig = sig.delete_if {|key, value| key == :controller} - return false unless sig == get_signature_hash(params, @secret) - return true - else - return false - end - end - - # params: - # params, Hash - # request, String - # returns: - # TrueClass or FalseClass, Returns true if the incoming request is an authenticated hardbounce post. - def receive_hardbounce_post(params, request) - if request.post? - [:action, :email, :sig].each { |key| return false unless params.has_key?(key) } - - return false unless params[:action] == 'hardbounce' - - sig = params.delete(:sig) - sig = sig.delete_if {|key, value| key == :controller} - return false unless sig == get_signature_hash(params, @secret) - return true - else - return false - end - end - - # params: - # email, String - # items, String - # incomplete, Integer - # message_id, String - # options, Hash - # returns: - # hash, response from server - # - # Record that a user has made a purchase, or has added items to their purchase total. - def purchase(email, items, incomplete = nil, message_id = nil, options = {}) - data = options - data[:email] = email - data[:items] = items - - if incomplete != nil - data[:incomplete] = incomplete.to_i - end - - if message_id != nil - data[:message_id] = message_id - end - api_post(:purchase, data) - end - - - # <b>DEPRECATED:</b> Please use either stats_list or stats_blast - # params: - # stat, String - # - # returns: - # hash, response from server - # Request various stats from Sailthru. - def get_stats(stat) - warn "[DEPRECATION] `get_stats` is deprecated. Please use `stats_list` and `stats_blast` instead" - api_get(:stats, {:stat => stat}) - end - - - # params - # list, String - # date, String - # - # returns: - # hash, response from server - # Retrieve information about your subscriber counts on a particular list, on a particular day. - def stats_list(list = nil, date = nil) - data = {} - if list != nil - data[:list] = list - end - if date != nil - data[:date] = date - end - data[:stat] = 'list' - stats(data) - end - - - # params - # blast_id, String - # start_date, String - # end_date, String - # options, Hash - # - # returns: - # hash, response from server - # Retrieve information about a particular blast or aggregated information from all of blasts over a specified date range - def stats_blast(blast_id = nil, start_date = nil, end_date = nil, options = {}) - data = options - if blast_id != nil - data[:blast_id] = blast_id - end - if start_date != nil - data[:start_date] = start_date - end - if end_date != nil - data[:end_date] = end_date - end - data[:stat] = 'blast' - stats(data) - end - - # params - # template, String - # start_date, String - # end_date, String - # options, Hash - # - # returns: - # hash, response from server - # Retrieve information about a particular blast or aggregated information from all of blasts over a specified date range - def stats_send(template = nil, start_date = nil, end_date = nil, options = {}) - data = options - if template != nil - data[:template] = template - end - if start_date != nil - data[:start_date] = start_date - end - if end_date != nil - data[:end_date] = end_date - end - data[:stat] = 'send' - stats(data) - end - - - # params - # title, String - # url, String - # date, String - # tags, Array or Comma separated string - # vars, Hash - # options, Hash - # - # Push a new piece of content to Sailthru, triggering any applicable alerts. - # http://docs.sailthru.com/api/content - def push_content(title, url, date = nil, tags = nil, vars = {}, options = {}) - data = options - data[:title] = title - data[:url] = url - if date != nil - data[:date] = date - end - if tags != nil - if tags.class == Array - tags = tags.join(',') - end - data[:tags] = tags - end - if vars.length > 0 - data[:vars] = vars - end - api_post(:content, data) - end - - # params - # list, String - # - # Get information about a list. - def get_list(list) - return api_get(:list, {:list => list}) - end - - # params - # - # Get information about all lists - def get_lists() - return api_get(:list, {}) - end - - # params - # list, String - # options, Hash - # Create a list, or update a list. - def save_list(list, options = {}) - data = options - data[:list] = list - return api_post(:list, data) - end - - # params - # list, String - # - # Deletes a list - def delete_list(list) - api_delete(:list, {:list => list}) - end - - # params - # email, String - # - # get user alert data - def get_alert(email) - api_get(:alert, {:email => email}) - end - - # params - # email, String - # type, String - # template, String - # _when, String - # options, hash - # - # Add a new alert to a user. You can add either a realtime or a summary alert (daily/weekly). - # _when is only required when alert type is weekly or daily - def save_alert(email, type, template, _when = nil, options = {}) - data = options - data[:email] = email - data[:type] = type - data[:template] = template - if (type == 'weekly' || type == 'daily') - data[:when] = _when - end - api_post(:alert, data) - end - - - # params - # email, String - # alert_id, String - # - # delete user alert - def delete_alert(email, alert_id) - data = {:email => email, :alert_id => alert_id} - api_delete(:alert, data) - end - - # Make Stats API Request - def stats(data) - api_get(:stats, data) - end - - # params - # job, String - # options, hash - # report_email, String - # postback_url, String - # binary_key, String - # - # interface for making request to job call - def process_job(job, options = {}, report_email = nil, postback_url = nil, binary_key = nil) - data = options - data['job'] = job - if !report_email.nil? - data['report_email'] = report_email - end - - if !postback_url.nil? - data['postback_url'] = postback_url - end - api_post(:job, data, binary_key) - end - - # params - # emails, String | Array - # implementation for import_job - def process_import_job(list, emails, report_email = nil, postback_url = nil, options = {}) - data = options - data['list'] = list - data['emails'] = Array(emails).join(',') - process_job(:import, data, report_email, postback_url) - end - - # implementation for import job using file upload - def process_import_job_from_file(list, file_path, report_email = nil, postback_url = nil, options = {}) - data = options - data['list'] = list - data['file'] = file_path - process_job(:import, data, report_email, postback_url, 'file') - end - - # implementation for update job using file upload - def process_update_job_from_file(file_path, report_email = nil, postback_url = nil, options = {}) - data = options - data['file'] = file_path - process_job(:update, data, report_email, postback_url, 'file') - end - - # implementation for snapshot job - def process_snapshot_job(query = {}, report_email = nil, postback_url = nil, options = {}) - data = options - data['query'] = query - process_job(:snapshot, data, report_email, postback_url) - end - - # implementation for export list job - def process_export_list_job(list, report_email = nil, postback_url = nil, options = {}) - data = options - data['list'] = list - process_job(:export_list_data, data, report_email, postback_url) - end - - # get status of a job - def get_job_status(job_id) - api_get(:job, {'job_id' => job_id}) - end - - # Get user by Sailthru ID - def get_user_by_sid(id, fields = {}) - api_get(:user, {'id' => id, 'fields' => fields}) - end - - # Get user by specified key - def get_user_by_key(id, key, fields = {}) - data = { - 'id' => id, - 'key' => key, - 'fields' => fields - } - api_get(:user, data) - end - - # Create new user, or update existing user - def save_user(id, options = {}) - data = options - data['id'] = id - api_post(:user, data) - end - - # params - # Get an existing trigger - def get_triggers() - api_get(:trigger, {}) - end - - # params - # template, String - # trigger_id, String - # Get an existing trigger - def get_trigger_by_template(template, trigger_id = nil) - data = {} - data['template'] = template - if trigger_id != nil then data['trigger_id'] = trigger_id end - api_get(:trigger, data) - end - - # params - # event, String - # Get an existing trigger - def get_trigger_by_event(event) - data = {} - data['event'] = event - api_get(:trigger, data) - end - - # params - # template, String - # time, String - # time_unit, String - # event, String - # zephyr, String - # Create or update a trigger - def post_template_trigger(template, time, time_unit, event, zephyr) - data = {} - data['template'] = template - data['time'] = time - data['time_unit'] = time_unit - data['event'] = event - data['zephyr'] = zephyr - api_post(:trigger, data) - end - - # params - # template, String - # time, String - # time_unit, String - # zephyr, String - # Create or update a trigger - def post_event_trigger(event, time, time_unit, zephyr) - data = {} - data['time'] = time - data['time_unit'] = time_unit - data['event'] = event - data['zephyr'] = zephyr - api_post(:trigger, data) - end - - # params - # id, String - # event, String - # options, Hash (Can contain vars, Hash and/or key) - # Notify Sailthru of an Event - def post_event(id, event, options = {}) - data = options - data['id'] = id - data['event'] = event - api_post(:event, data) - end - - # Perform API GET request - def api_get(action, data) - api_request(action, data, 'GET') - end - - # Perform API POST request - def api_post(action, data, binary_key = nil) - api_request(action, data, 'POST', binary_key) - end - - #Perform API DELETE request - def api_delete(action, data) - api_request(action, data, 'DELETE') - end - - protected - - # params: - # action, String - # data, Hash - # request, String "GET" or "POST" - # returns: - # Hash - # - # Perform an API request, using the shared-secret auth hash. - # - def api_request(action, data, request_type, binary_key = nil) - if (!binary_key.nil?) - binary_key_data = data[binary_key] - data.delete(binary_key) - end - - if data[:format].nil? or data[:format] == 'json' - data = self.prepare_json_payload(data) - else - data[:api_key] = @api_key - data[:format] ||= 'json' - data[:sig] = get_signature_hash(data, @secret) - end - - if (!binary_key.nil?) - data[binary_key] = binary_key_data - end - _result = self.http_request("#{@api_uri}/#{action}", data, request_type, binary_key) - - # NOTE: don't do the unserialize here - if data[:format] == 'json' - begin - unserialized = JSON.parse(_result) - return unserialized ? unserialized : _result - rescue JSON::JSONError => e - return {'error' => e} - end - end - return _result - end - - # set up our post request - def set_up_post_request(uri, data, headers, binary_key = nil) - if (!binary_key.nil?) - binary_data = data[binary_key] - - if binary_data.is_a?(StringIO) - data[binary_key] = UploadIO.new( - binary_data, "text/plain", "local.path" - ) - else - data[binary_key] = UploadIO.new( - File.open(binary_data), "text/plain" - ) - end - - req = Net::HTTP::Post::Multipart.new(uri.path, data) - else - req = Net::HTTP::Post.new(uri.path, headers) - req.set_form_data(data) - end - req - end - - # params: - # uri, String - # data, Hash - # method, String "GET" or "POST" - # returns: - # String, body of response - def http_request(uri, data, method = 'POST', binary_key = nil) - data = flatten_nested_hash(data, false) - - if method != 'POST' - uri += "?" + data.map{ |key, value| "#{CGI::escape(key.to_s)}=#{CGI::escape(value.to_s)}" }.join("&") - end - - req = nil - headers = {"User-Agent" => "Sailthru API Ruby Client #{VERSION}"} - - _uri = URI.parse(uri) - - if method == 'POST' - req = self.set_up_post_request( - _uri, data, headers, binary_key - ) - - else - request_uri = "#{_uri.path}?#{_uri.query}" - if method == 'DELETE' - req = Net::HTTP::Delete.new(request_uri, headers) - else - req = Net::HTTP::Get.new(request_uri, headers) - end - end - - begin - http = Net::HTTP::Proxy(@proxy_host, @proxy_port).new(_uri.host, _uri.port) - - if _uri.scheme == 'https' - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl != true # some openSSL client doesn't work without doing this - http.ssl_timeout = @opts[:http_ssl_timeout] || 5 - end - http.open_timeout = @opts[:http_open_timeout] || 5 - http.read_timeout = @opts[:http_read_timeout] || 10 - http.close_on_empty_response = @opts[:http_close_on_empty_response] || true - - response = http.start { - http.request(req) - } - - rescue Timeout::Error, Errno::ETIMEDOUT => e - raise SailthruUnavailableException.new(["Timed out: #{_uri}", e.inspect, e.backtrace].join("\n")); - rescue Exception => e - raise SailthruClientException.new(["Unable to open stream: #{_uri}", e.inspect, e.backtrace].join("\n")); - end - - if response.body - return response.body - else - raise SailthruClientException.new("No response received from stream: #{_uri}") - end - end - - def http_multipart_request(uri, data) - Net::HTTP::Post::Multipart.new url.path, - "file" => UploadIO.new(data['file'], "application/octet-stream") - end - - def prepare_json_payload(data) - payload = { - :api_key => @api_key, - :format => 'json', #<3 XML - :json => data.to_json - } - payload[:sig] = get_signature_hash(payload, @secret) - payload - end + def self.secret + @secret end end