lib/phaxio/client.rb in phaxio-0.5.0 vs lib/phaxio/client.rb in phaxio-2.0.0

- old
+ new

@@ -1,502 +1,135 @@ +# @!macro [new] paging +# @note +# This action accepts paging parameters: +# - *per_page* [Integer] - The maximum number of results to return per +# call (i.e. "page"). Max 1000. +# - *page* [Integer] - The page number to return for the request. 1-based. + module Phaxio - include HTTMultiParty - base_uri 'https://api.phaxio.com/v1' + # @api private - module Config - attr_accessor :api_key, :api_secret, :callback_token - end + class Client + class << self + # Makes a request to the Phaxio API. + # + # @param method [Symbol, String] + # The HTTP method for the request. Currently only `:get`, `:post`, and `:delete` are + # supported. + # @param endpoint [String] + # The endpoint for the API action, relative to `Phaxio::Config.api_endpoint`. + # @param params [Hash] + # Any parameters to be sent with the request. + # + # @return [Object] + # The `"data"` attribute of the deserialized JSON response. Varies based on the API action. + def request method, endpoint, params = {} + params = api_params params + begin + response = case method.to_s + when 'post' then post(endpoint, params) + when 'get' then get(endpoint, params) + when 'delete' then delete(endpoint, params) + else raise ArgumentError, "HTTP method `#{method}` is not supported." + end + handle_response response + rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => error + raise Error::ApiConnectionError, "Error communicating with Phaxio: #{error}" + end + end - module Client - DIGEST = OpenSSL::Digest.new('sha1') + # @return [Faraday::Connection] A new Faraday connection to `Phaxio::Config.api_endpoint`. + def conn + Faraday.new(Phaxio.api_endpoint) do |conn| + conn.request :multipart + conn.request :url_encoded + conn.adapter :net_http + end + end - # Public: Send a fax. - # - # options - The Hash options used to refine the selection (default: {}): - # :to - The Phone Number (i.e. [country - # code][number] or just a 10 digit - # number in the US or Canada). Put - # square brackets after parameter - # name to send to multiple - # recipients (e.g. to[]) (required). - # :filename - A Ruby File of a document to fax - # (supported file types: https://www.phaxio.com/faq#faq10) - # :string_data - A String of html, plain text, or a - # URL. If additional files are - # specified as well, this data will - # be included first in the fax - # (optional). - # :string_data_type - An enum of the type of the string - # data that can be 'html', 'url', or - # 'text'. If not specified, default - # is 'text'. See string data - # rendering for more info (optional). - # :batch - The bool for running in batching - # mode. If present and true, fax will - # be sent in batching mode. Requires - # batch_delay to be specified - # (optional). - # :batch_delay - The int the of amount of time, in - # seconds, before the batch is fired. - # Must be specified if batch=true. - # Maximum delay is 3600 (1 hour) - # (optional). - # :batch_collision_avoidance - The bool for collision avoidance - # with batches. If true when - # batch=true, fax will be blocked - # until the receiving machine is no - # longer busy (optional). - # :callback_url - The String url for the callback. - # Overrides the globally set one - # (optional). - # :cancel_timeout - An int of the number of minutes - # after which the fax will be - # canceled if it hasn't yet - # completed. Must be between 1 and 60 - # (optional). - # - # Examples - # - # Phaxio.send_fax(to: "0123456789", filename: File.new("docToSend.pdf")) - # - # Returns a HTTParty::Response object containing a success bool, - # a String message, and an in faxID. - def send_fax(options) - send_post("/send", options) - end + private - # Public: Resend a fax. - # - # options - The Hash options used to refine the selection (default: {}): - # :id - The int id of the fax you want to resend (required). - # - # Examples - # - # Phaxio.resend_fax(id: "123456") - # - # Returns a HTTParty::Response object containing a success bool, - # a message string, and data containing the fax ID int. - def resend_fax(options) - send_post("/resendFax", options) - end + def handle_response response + content_type = response.headers[:content_type] - # Public: Test receiving a fax. - # - # options - The Hash options used to refine the selection (default: {}): - # from_number - The Phone Number of the simulated sender - # (optional). - # to_number - The Phone Number receiving the fax (optional). - # filename - A String containing the name of the PDF that has - # a PhaxCode and is the file you want to simulate - # sending (required). - # - # Examples - # - # Phaxio.test_receive(filename: "test_file.pdf") - # - # Returns a HTTParty::Response object containing a success bool - # and a String message. - def test_receive(options) - send_post("/testReceive", options) - end + if content_type.start_with? 'application/json' + body = JSON.parse(response.body).with_indifferent_access + else + extension = MimeTypeHelper.extension_for_mimetype content_type + filename = File.join( + Dir.tmpdir, + Dir::Tmpname.make_tmpname('phaxio-', "download.#{extension}") + ) + File.open(filename, 'wb') { |file| file.write response.body } + body = {'success' => response.success?, 'data' => File.open(filename, 'rb')} + end - # Public: Provision a phone number that you can use to receive faxes in - # your Phaxio account. - # - # options - The Hash options used to refine the selection (default: {}): - # area_code - The integer area code of the number you'd like - # to provision (required). - # callback_url - A callback URL that Phaxio will post to when a - # fax is received by this number. This will - # override the global receive callback URL, if you - # have one set (optional). - # - # Examples - # - # Phaxio.provision_number(area_code: 802) - # - # Returns a HTTParty::Response object containing a success bool, a string - # message, and data containing the phone number, city, state, cost, - # last_billed_at, and the date the number was provisioned at. - def provision_number(options) - send_post("/provisionNumber", options) - end + if response.success? + raise(Error::GeneralError, body['message']) unless body['success'] - # Public: Release a phone number that you no longer need. Once a phone - # number is released you will no longer be charged for it. - # - # options - The Hash options used to refine the selection (default: {}): - # number - The String of the phone number you want to release - # (required). - # - # Examples - # - # Phaxio.release_number(number: "8021112222") - # - # Returns a HTTParty::Response object containing a success bool and a - # string message. - def release_number(options) - send_post("/releaseNumber", options) - end + # Check if this is a response with paging. If so, we want to return that along with the + # data. + if body.key? 'paging' + {'data' => body['data'], 'paging' => body['paging']} + else + body['data'] + end + else + status = response.status + # TODO: Handle blank message + message = body['message'] - # Public: Get a detailed list of the phone numbers you current own on - # Phaxio. - # - # options - The Hash options used to refne th selection (default: {}): - # area_code - An integer area code you'd like to filter by - # (optional). - # number - A String phone number you'd like to retrieve - # (optional). - # - # Examples - # - # Phaxio.list_numbers # list all the numbers you own - # - # Phaxio.list_numbers(area_code: 802) # list all numbers in the 802 area - # - # Phaxio.list_numbers(number: "8021112222") # show specific number detail - # - # Returns a HTTParty::Reponse object containing a success bool, a message, - # and the data attributes containing the queried phone number(s) details. - def list_numbers(options = {}) - send_post("/numberList", options) - end + case status + when 401 + raise Error::AuthenticationError, "#{status}: #{message}" + when 404 + raise Error::NotFoundError, "#{status}: #{message}" + when 422 + raise Error::InvalidRequestError, "#{status}: #{message}" + when 429 + raise Error::RateLimitExceededError, "#{status}: #{message}" + else + raise Error::GeneralError, "#{status}: #{message}" + end + end + end - # Public: Get an image thumbnail or PDF file for a fax. For images to work - # file storage must not be disabled with Phaxio. - # - # options - The Hash options used to refine the selection (default: {}): - # id - The integer fax id of the fax you wish to retreive - # (required). - # type - An enum for the type return, defaults to 'p' (optional): - # s - Small JPG format thumbnail of the fax, 129 x 167 px. - # l - Large JPG format thumbnail of the fax, 300 x 388 px. - # p - PDF version of the fax (default). - # - # Examples - # - # Phaxio.get_fax_file(id: 1234, type: p) - # Phaxio.get_fax_file(id: 3254, type: l) - # - # Returns the fax as the type specified in the call, defaults to PDF. - def get_fax_file(options) - send_post("/faxFile", options) - end + def post endpoint, params = {} + # Handle file params + params.each do |k, v| + next unless k.to_s == 'file' + mime_type = MimeTypeHelper.mimetype_for_file v.path + params[k] = Faraday::UploadIO.new v, mime_type + end - # Public: List faxes within the specified time range. - # - # options - The Hash options used to refine the selection (default: {}): - # start - The Unix Timestamp for the beginning of the range - # (required). - # end - The Unix Timestamp for the end of the range (required). - # - # Examples - # - # Phaxio.list_faxes(start: 1293861600, end: 1294034400) - # - # Returns a HTTParty::Response object containing a success bool, a string - # message, paging information, and the fax data. - def list_faxes(options) - send_post("/faxList", options) - end - - # Public: Get the status of a specific fax. - # - # options - The Hash options used to refine the selection (default: {}): - # id - The int id of the fax you want to get the status of - # (required). - # - # Examples - # - # Phaxio.get_fax_status(id: "123456") - # - # Returns a HTTParty::Response object containing a success bool, - # a String message, and the data of the fax. - def get_fax_status(options) - if options[:id].nil? - raise StandardError, "You must include a fax id." + conn.post endpoint, params end - send_post("/faxStatus", options) - end - - # Public: Cancel a specific fax. - # - # options - The Hash options used to refine the selection (defaults: {}): - # id - The int id of the fax you want to cancel (required). - # - # Examples - # - # Phaxio.cancel_fax(id: "123456") - # - # Returns a HTTParty::Response object containing a success bool - # and a String message. - def cancel_fax(options) - send_post("/faxCancel", options) - end - - # Public: Delete a specific fax. - # - # options - The hash options used to refine the selection (defaults: {}): - # :id - The int ID of the fax you want to cancel - # (required). - # :files_only - The bool used to determine whether only the files - # are deleted. If not specified, default is false - # (optional). - # - # Examples - # - # Phaxio.delete_fax(id: 1234, files_only: true) - # - # Returns a HTTParty::Response object with success bool and message string. - def delete_fax(options) - send_post("/deleteFax", options) - end - - # Public: Get the status of Client's account. - # - # Examples - # - # Phaxio.get_account_status - # - # Returns a HTTParty::Response object with success, message, and data - # (containing faxes_sent_this_month, faxes_sent_today, and balance). - def get_account_status - send_post("/accountStatus", {}) - end - - # Public: Attach a PhaxCode to a PDF you provide. - # - # options - Type: hash. Options used to refine the action (default: {}): - # x - Type: float. The x-coordinate (in PDF points*) - # where the PhaxCode should be drawn. x=0 is at the - # left-most point on the page. (required) - # y - Type: float. The y-coordinate (in PDF points*) - # where the PhaxCode should be drawn. Y=0 is the - # bottom-most point on the page. (required) - # filename - A Ruby File in PDF format (required) - # metadata - Type: string. Custom metadata to be associated - # with the created barcode. If not present, the - # basic PhaxCode for your account will be used. - # page_number - Type: integer. The page where the PhaxCode should - # be drawn. 1-based. - # *PDF points definition: A "point" is 1/72 of an inch. An - # 8.5"x11" document is therefore 612 pt x 792 pt. - # - # Examples - # - # Phaxio.attach_phaxcode_to_pdf( - # x: "0", y: "100", filename: File.new("input.pdf") - # ) - # - # Response: A PDF file containing a PhaxCode at the location specified. - def attach_phaxcode_to_pdf(options) - if options[:filename].nil? - raise StandardError, 'You must include a PDF file.' + def get endpoint, params = {} + conn.get endpoint, params end - if options[:x] < 0 || options[:y] < 0 - raise StandardError, 'Coordinates must be greater than or equal to 0.' + def delete endpoint, params = {} + conn.delete endpoint, params end - send_post('/attachPhaxCodeToPdf', options) - end + def api_params params + params = default_params.merge params - # Public: Create a custom PhaxCode. - # - # options - Type: hash. Options used to refine the action (default: {}): - # metadata - Type: string. Custom metadata to be associated with - # this barcode. If not present, the basic PhaxCode for - # your account will be used. (optional) - # redirect - Type: boolean. If present and true, the PhaxCode - # barcode image will be dumped in the response. - # (optional) - # - # Example: - # Phaxio.create_phaxcode(metadata: "sale_id=44") - # - # Response: If the redirect parameter is not provided, a JSON object with - # success, message, and data attributes is returned. The data - # attribute contains a url where the PhaxCode barcode image can be - # accessed. Otherwise, the image data is dumped in the response. - def create_phaxcode(options = {}) - send_post('/createPhaxCode', options) - end + # Convert times to ISO 8601 + params.each do |k, v| + next unless v.kind_of?(Time) || v.kind_of?(Date) + params[k] = v.to_datetime.iso8601 + end - # Public: Get a Hosted Document with PhaxCode included - # - # Note: You will have to set up the hosted document with Phaxio (along with - # the relevant PhaxCode) before calling this method. - # - # options - Type: hash. Options used to refine the action (default: {}): - # name - Type: string. The name of a hosted document. - # (required) - # metadata - Type: string. Custom metadata to be associated with - # the PhaxCode that will be attached to the hosted - # document. If not present, the basic PhaxCode for your - # account will be used. - # (optional) - # - # Example: - # Phaxio.get_hosted_document(name:"business_fax") - # - # Response: A PDF copy of the hosted document with a PhaxCode included at - # the pre-specified location. - def get_hosted_document(options) - if options[:name].nil? - raise StandardError, 'You must include the name of the hosted document.' + params end - send_post('/getHostedDocument', options) + def default_params + { + api_key: Phaxio.api_key, + api_secret: Phaxio.api_secret + } + end end - - # Public: Get a list of supported countries for sending faxes - # - # Note: This method doesn't require API keys and is included for the sake of - # completion. - # - # Example: - # Phaxio.supported_countries - # - # Response: A JSON object with success, message, and data attributes. The - # data attribute contains a hash, where the key contains the name - # of the country, and the value is a hash of attributes for the - # country (currently only pricing information). - # - # Example Response: - # { - # "success": true, - # "message": "Data contains supported countries.", - # "data": { - # "United States": { - # "price_per_page": 7 - # }, - # "Canada": { - # "price_per_page": 7 - # }, - # "United Kingdom": { - # "price_per_page": 10 - # }, - # ... - # } - # } - def supported_countries - post('/supportedCountries') - end - - # Public: List area codes available for purchasing numbers - # - # Note: This method doesn't require API keys and is included for the sake of - # completion. - # - # options - Type: hash. Options used to refine the query (default: {}): - # is_toll_free - Type: boolean. Will only return toll free area - # codes. (optional) - # state - Type: string. A two character state or province - # abbreviation (e.g. IL or YT). Will only return - # area codes for this state. (optional) - # - # Response: A JSON object with success, message, and data attributes. The - # data attribute contains a map of area codes to city and state. - # - # Example response: - # { - # "success": true, - # "message": "295 area codes available.", - # "data": { - # "201": { - # "city": "Bayonne, Jersey City, Union City", - # "state": "New Jersey" - # }, - # "202": { - # "city": "Washington", - # "state": "District Of Columbia" - # }, - # ... a lot more area codes here... - # } - # } - def area_codes(options = {}) - post('/areaCodes', options) - end - - def send_post(path, options) - post( - path, query: options.merge!(api_key: api_key, api_secret: api_secret) - ) - end - - # Public: Check the signature of the signed request. - # - # signature - Type: string. The X-Phaxio-Signature HTTP header value. - # url - Type: string. The full URL that was called by Phaxio, - # including the query. (required) - # params - Type: hash. The POSTed form data (required) - # files - Type: array. Submitted files (required - "received" fax - # callback only) - # - # Returns true if the signature matches the signed request, otherwise false - def valid_callback_signature?(signature, url, params, files = []) - check_signature = generate_check_signature(url, params, files) - check_signature == signature - end - - # Public: Generate a signature using the request data and callback token - # - # url - Type: string. The full URL that was called by Phaxio, - # including the query. (required) - # params - Type: hash. The POSTed form data (required) - # files - Type: array. Submitted files (required - "received" fax - # callback only) - # - # Retuns a signature based on the request data and configured callback - # token, which can then be compared with the request signature. - def generate_check_signature(url, params, files = []) - params_string = generate_params_string(params) - file_string = generate_files_string(files) - callback_data = "#{url}#{params_string}#{file_string}" - OpenSSL::HMAC.hexdigest(DIGEST, callback_token, callback_data) - end - - private - - def generate_params_string(params) - sorted_params = params.sort_by { |key, _value| key } - params_strings = sorted_params.map { |key, value| "#{key}#{value}" } - params_strings.join - end - - def generate_files_string(files) - files_array = files_to_array(files).reject(&:nil?) - sorted_files = files_array.sort_by { |file| file[:name] } - files_strings = sorted_files.map { |file| generate_file_string(file) } - files_strings.join - end - - def files_to_array(files) - files.is_a?(Array) ? files : [files] - end - - def generate_file_string(file) - file[:name] + DIGEST.hexdigest(file[:tempfile].read) - end end - - # Public: Configure Phaxio with your api_key, api_secret, and the callback - # token provided in your Phaxio account (to verify that requests are - # coming from Phaxio). - # - # Examples - # - # Phaxio.config do |config| - # config.api_key = '12345678910' - # config.api_secret = '10987654321' - # config.callback_token = '32935829' - # end - # - # Returns nothing. - def self.config - yield(self) - end - - extend Client - extend Config end