sferik/twitter

View on GitHub
lib/twitter/rest/request.rb

Summary

Maintainability
A
1 hr
Test Coverage
require "addressable/uri"
require "http"
require "http/form_data"
require "json"
require "openssl"
require "twitter/error"
require "twitter/headers"
require "twitter/rate_limit"
require "twitter/utils"
require "twitter/rest/form_encoder"

module Twitter
  module REST
    class Request # rubocop:disable Metrics/ClassLength
      include Twitter::Utils
      BASE_URL = "https://api.twitter.com".freeze
      attr_accessor :client, :headers, :options, :path, :rate_limit,
                    :request_method, :uri
      alias verb request_method

      # @param client [Twitter::Client]
      # @param request_method [String, Symbol]
      # @param path [String]
      # @param options [Hash]
      # @return [Twitter::REST::Request]
      def initialize(client, request_method, path, options = {}, params = nil)
        @client = client
        @uri = Addressable::URI.parse(path.start_with?("http") ? path : BASE_URL + path)
        multipart_options = params || options
        set_multipart_options!(request_method, multipart_options)
        @path = uri.path
        @options = options
        @options_key = {get: :params, json_post: :json, json_put: :json, delete: :params}[request_method] || :form
        @params = params
      end

      # @return [Array, Hash]
      def perform
        response = http_client.headers(@headers).public_send(@request_method, @uri.to_s, request_options)
        response_body = response.body.empty? ? "" : symbolize_keys!(response.parse)
        response_headers = response.headers
        fail_or_return_response_body(response.code, response_body, response_headers)
      end

    private

      def request_options
        options = if @options_key == :form
                    {form: HTTP::FormData.create(@options, encoder: Twitter::REST::FormEncoder.method(:encode))}
                  else
                    {@options_key => @options}
                  end

        if @params
          if options[:params]
            options[:params].merge(@params)
          else
            options[:params] = @params
          end
        end
        options
      end

      def merge_multipart_file!(options)
        key = options.delete(:key)
        file = options.delete(:file)

        options[key] = if file.is_a?(StringIO)
                         HTTP::FormData::File.new(file, content_type: "video/mp4")
                       else
                         HTTP::FormData::File.new(file, filename: File.basename(file), content_type: content_type(File.basename(file)))
                       end
      end

      def set_multipart_options!(request_method, options)
        if %i[multipart_post json_post].include?(request_method)
          merge_multipart_file!(options) if request_method == :multipart_post
          options = {}
          @request_method = :post
        elsif request_method == :json_put
          @request_method = :put
        else
          @request_method = request_method
        end
        @headers = Twitter::Headers.new(@client, @request_method, @uri, options).request_headers
      end

      def content_type(basename)
        case basename
        when /\.gif$/i
          "image/gif"
        when /\.jpe?g/i
          "image/jpeg"
        when /\.png$/i
          "image/png"
        else
          "application/octet-stream"
        end
      end

      def fail_or_return_response_body(code, body, headers)
        error = error(code, body, headers)
        raise(error) if error

        @rate_limit = Twitter::RateLimit.new(headers)
        body
      end

      def error(code, body, headers)
        klass = Twitter::Error::ERRORS[code]
        if klass == Twitter::Error::Forbidden
          forbidden_error(body, headers)
        elsif !klass.nil?
          klass.from_response(body, headers)
        elsif body.is_a?(Hash) && (err = body.dig(:processing_info, :error))
          Twitter::Error::MediaError.from_processing_response(err, headers)
        end
      end

      def forbidden_error(body, headers)
        error = Twitter::Error::Forbidden.from_response(body, headers)
        klass = Twitter::Error::FORBIDDEN_MESSAGES[error.message]
        if klass
          klass.from_response(body, headers)
        else
          error
        end
      end

      def symbolize_keys!(object)
        case object
        when Array
          object.each_with_index do |val, index|
            object[index] = symbolize_keys!(val)
          end
        when Hash
          object.dup.each_key do |key|
            object[key.to_sym] = symbolize_keys!(object.delete(key))
          end
        end
        object
      end

      # Returns boolean indicating if all the keys required by HTTP::Client are present in Twitter::Client#timeouts
      #
      # @return [Boolean]
      def timeout_keys_defined
        (%i[write connect read] - (@client.timeouts&.keys || [])).empty?
      end

      # @return [HTTP::Client, HTTP]
      def http_client
        client = @client.proxy ? HTTP.via(*proxy) : HTTP
        client = client.timeout(connect: @client.timeouts[:connect], read: @client.timeouts[:read], write: @client.timeouts[:write]) if timeout_keys_defined
        client
      end

      # Return proxy values as a compacted array
      #
      # @return [Array]
      def proxy
        @client.proxy.values_at(:host, :port, :username, :password).compact
      end
    end
  end
end