# frozen_string_literal: true require "net/https" module Discorb # # A class to handle http requests. # @private # class HTTP @nil_body = nil # @private def initialize(client) @client = client @ratelimit_handler = RatelimitHandler.new(client) end # # Execute a request. # @async # # @param [Discorb::Route] path The path to the resource. # @param [String, Hash] body The body of the request. Defaults to an empty string. # @param [Hash] headers The headers to send with the request. # @param [String] audit_log_reason The audit log reason to send with the request. # @param [Hash] kwargs The keyword arguments. # # @return [Async::Task<Array(Net::HTTPResponse, Hash)>] The response and as JSON. # @return [Async::Task<Array(Net::HTTPResponse, nil)>] The response was 204. # # @raise [Discorb::HTTPError] The request was failed. # def request(path, body = "", headers: nil, audit_log_reason: nil, **kwargs) Async do |task| @ratelimit_handler.wait(path) if %i[post patch put].include? path.method resp = http.send(path.method, get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs) else resp = http.send(path.method, get_path(path), get_headers(headers, body, audit_log_reason), **kwargs) end data = get_response_data(resp) @ratelimit_handler.save(path, resp) handle_response(resp, data, path, body, headers, audit_log_reason, kwargs) end end # # Execute a multipart request. # @async # # @param [Discorb::Route] path The path to the resource. # @param [String, Hash] body The body of the request. # @param [Array<Discorb::File>] files The files to upload. # @param [Hash] headers The headers to send with the request. # @param [String] audit_log_reason The audit log reason to send with the request. # @param [Hash] kwargs The keyword arguments. # # @return [Async::Task<Array(Net::HTTPResponse, Hash)>] The response and as JSON. # @return [Async::Task<Array(Net::HTTPResponse, nil)>] The response was 204. # # @raise [Discorb::HTTPError] The request was failed. # def multipart_request(path, body = "", files, headers: nil, audit_log_reason: nil, **kwargs) Async do |task| @ratelimit_handler.wait(path) req = Net::HTTP.const_get(path.method.to_s.capitalize).new(get_path(path), get_headers(headers, body, audit_log_reason), **kwargs) data = [ ["payload_json", get_body(body)], ] files&.each_with_index do |file, i| next if file.nil? data << ["files[#{i}]", file.io, { filename: file.filename, content_type: file.content_type }] end req.set_form(data, "multipart/form-data") session = Net::HTTP.new("discord.com", 443) session.use_ssl = true resp = session.request(req) data = get_response_data(resp) @ratelimit_handler.save(path, resp) handle_response(resp, data, path, body, headers, audit_log_reason, kwargs) end end def inspect "#<#{self.class} client=#{@client}>" end private def handle_response(resp, data, path, body, headers, audit_log_reason, kwargs) case resp.code when "429" @client.log.info("Rate limit exceeded for #{path.method} #{path.url}, waiting #{data[:retry_after]} seconds") sleep(data[:retry_after]) request(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait when "400" raise BadRequestError.new(resp, data) when "401" raise UnauthorizedError.new(resp, data) when "403" raise ForbiddenError.new(resp, data) when "404" raise NotFoundError.new(resp, data) else [resp, data] end end def get_headers(headers, body = "", audit_log_reason = nil) ret = if body.nil? || body == "" { "User-Agent" => USER_AGENT, "authorization" => "Bot #{@client.token}" } else { "User-Agent" => USER_AGENT, "authorization" => "Bot #{@client.token}", "content-type" => "application/json" } end ret.merge!(headers) if !headers.nil? && headers.length.positive? ret["X-Audit-Log-Reason"] = audit_log_reason unless audit_log_reason.nil? ret end def get_body(body) if body.nil? "" elsif body.is_a?(String) body else recr_utf8(body).to_json end end def get_path(path) full_path = if path.url.start_with?("https://") path.url else API_BASE_URL + path.url end uri = URI(full_path) full_path.sub(uri.scheme + "://" + uri.host, "") end def get_response_data(resp) begin data = JSON.parse(resp.body, symbolize_names: true) rescue JSON::ParserError, TypeError if resp.body.nil? || resp.body.empty? data = nil else data = resp.body end end if resp["Via"].nil? && resp.code == "429" && data.is_a?(String) raise CloudFlareBanError.new(resp, @client) end data end def http https = Net::HTTP.new("discord.com", 443) https.use_ssl = true https end def recr_utf8(data) case data when Hash data.each do |k, v| data[k] = recr_utf8(v) end data when Array data.each_index do |i| data[i] = recr_utf8(data[i]) end data when String data.dup.force_encoding(Encoding::UTF_8) else data end end end end