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