x 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ff134774b6d6b28f3f72c740c74b1f44b11ce94cff63fbfe4eabc0414f600c3
4
- data.tar.gz: 9f3b9abd3ca2628979676b5f634781b960adf6afe8a77559cf6f3a96ee738098
3
+ metadata.gz: 4a01e8a21951f3b998cfae0253c4d35c5412ac39f94681283b9059d4a196651e
4
+ data.tar.gz: 5b4a5bb02d86b27391b005acb329893025222c20e9d0c99b88ed0d7e6322dc5e
5
5
  SHA512:
6
- metadata.gz: cd471d0b216161f5e68832e83b5024a8a89cdda1c815f49543f8361b225776ad46f84690eddf9a9b884cf0ab540924ecae94ebd145f26ac07e44ffdf7c4ff1f2
7
- data.tar.gz: 3670666ca95e154a253be800af600cdcaf44195b9dcd3d001e1404d85f27dd5939ce09bd25588cfbe81cefef92ce22bfcd26e105dcb70d813bc4efa221a4360d
6
+ metadata.gz: 8972eafdb4c041a61258f409fef5a93dc0ff56b70e5c8dd384e126652c1ab32a4e52dc56bd253d7c204ea6313e3ac321e93c7245c722c0d5491b59443c63435a
7
+ data.tar.gz: 4a5dfb958b61b78cdcf9312250d3ba00182894241da1c3e9114f08f1029fb174be07043f607a864986e3d09d6d308dfac4d13549e2483d5746235b34c4dd05a2
data/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
- ## [Unreleased]
1
+ ## [0.10.0] - 2023-10-08
2
+
3
+ - Add media upload helper methods (6c6a267)
4
+ - Add PayloadTooLargeError class (cd61850)
5
+
6
+ ## [0.9.1] - 2023-10-06
7
+
8
+ - Allow successful empty responses (06bf7db)
9
+ - Update default User-Agent string (296b36a)
10
+ - Move query parameter escaping into RequestBuilder (56d6bd2)
2
11
 
3
12
  ## [0.9.0] - 2023-09-26
4
13
 
data/README.md CHANGED
@@ -90,6 +90,14 @@ Building and maintaining an open-source project like this takes a considerable a
90
90
 
91
91
  [Click here to sponsor this project.](https://github.com/sponsors/sferik)
92
92
 
93
+ ## Sponsors
94
+
95
+ Many thanks to our sponsors (listed in order of when they sponsored this project):
96
+
97
+ <a href="https://betterstack.com"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/better_stack.svg" alt="Better Stack" width="200" align="middle"></a>
98
+ <img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
99
+ <a href="https://sentry.io"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/sentry.svg" alt="Sentry" width="200" align="middle"></a>
100
+
93
101
  ## Development
94
102
 
95
103
  1. Checkout and repo:
data/lib/x/client.rb CHANGED
@@ -19,6 +19,9 @@ module X
19
19
  def_delegators :@request_builder, :content_type=, :user_agent=
20
20
  def_delegators :@response_handler, :array_class, :object_class
21
21
  def_delegators :@response_handler, :array_class=, :object_class=
22
+ alias_method :base_url, :base_uri
23
+ alias_method :base_url=, :base_uri=
24
+ attr_accessor :authenticator, :connection, :request_builder, :redirect_handler, :response_handler
22
25
 
23
26
  def initialize(bearer_token: nil,
24
27
  api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
data/lib/x/connection.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "forwardable"
2
2
  require "net/http"
3
+ require "openssl"
3
4
  require "uri"
4
5
  require_relative "errors/network_error"
5
6
 
@@ -16,8 +17,10 @@ module X
16
17
  DEFAULT_WRITE_TIMEOUT = 60 # seconds
17
18
  NETWORK_ERRORS = [
18
19
  Errno::ECONNREFUSED,
20
+ Errno::ECONNRESET,
19
21
  Net::OpenTimeout,
20
- Net::ReadTimeout
22
+ Net::ReadTimeout,
23
+ OpenSSL::SSL::SSLError
21
24
  ].freeze
22
25
 
23
26
  attr_reader :base_uri, :proxy_uri, :http_client
@@ -39,9 +42,9 @@ module X
39
42
  end
40
43
 
41
44
  def send_request(request)
42
- @http_client.request(request)
45
+ response = @http_client.request(request)
43
46
  rescue *NETWORK_ERRORS => e
44
- raise NetworkError, "Network error: #{e.message}"
47
+ raise NetworkError.new("Network error: #{e.message}", response: response)
45
48
  end
46
49
 
47
50
  def base_uri=(base_url)
@@ -56,6 +59,17 @@ module X
56
59
  @http_client.instance_variable_get(:@debug_output)
57
60
  end
58
61
 
62
+ def configuration
63
+ {
64
+ base_url: base_uri.to_s,
65
+ open_timeout: open_timeout,
66
+ read_timeout: read_timeout,
67
+ write_timeout: write_timeout,
68
+ proxy_url: proxy_uri.to_s,
69
+ debug_output: debug_output
70
+ }
71
+ end
72
+
59
73
  private
60
74
 
61
75
  def apply_http_client_settings(open_timeout:, read_timeout:, write_timeout:, debug_output:)
@@ -84,15 +98,15 @@ module X
84
98
  end
85
99
 
86
100
  def build_http_client(host:, port:)
87
- if @proxy_uri.nil?
88
- Net::HTTP.new(host, port)
89
- else
101
+ if defined?(@proxy_uri)
90
102
  Net::HTTP.new(host, port, @proxy_uri&.host, @proxy_uri&.port, @proxy_uri&.user, @proxy_uri&.password)
103
+ else
104
+ Net::HTTP.new(host, port)
91
105
  end
92
106
  end
93
107
 
94
108
  def conditionally_apply_http_client_settings
95
- if @http_client
109
+ if defined?(@http_client)
96
110
  settings = current_http_client_settings
97
111
  yield
98
112
  apply_http_client_settings(**settings)
@@ -1,15 +1,12 @@
1
1
  require "json"
2
- require "net/http"
3
2
 
4
3
  module X
5
4
  # Base error class
6
5
  class Error < ::StandardError
7
- JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
8
-
9
6
  attr_reader :object
10
7
 
11
8
  def initialize(msg, response:)
12
- @object = JSON.parse(response.body || "{}") if JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
9
+ @object = JSON.parse(response.body) if response&.body && !response.body.empty?
13
10
  super(msg)
14
11
  end
15
12
  end
@@ -0,0 +1,5 @@
1
+ require_relative "server_error"
2
+
3
+ module X
4
+ class InternalServerError < ServerError; end
5
+ end
@@ -1,5 +1,5 @@
1
- require_relative "error"
1
+ require_relative "server_error"
2
2
 
3
3
  module X
4
- class NetworkError < StandardError; end
4
+ class NetworkError < Error; end
5
5
  end
@@ -0,0 +1,5 @@
1
+ require_relative "client_error"
2
+
3
+ module X
4
+ class PayloadTooLargeError < ClientError; end
5
+ end
@@ -0,0 +1,125 @@
1
+ require "securerandom"
2
+
3
+ module X
4
+ # Helper module for uploading images and videos
5
+ module MediaUpload
6
+ extend self
7
+
8
+ MAX_RETRIES = 3
9
+ BYTES_PER_MB = 1_048_576
10
+ MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
11
+ DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MEDIA_CATEGORIES
12
+ DEFAULT_MIME_TYPE = "application/octet-stream".freeze
13
+ MIME_TYPES = %w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
14
+ GIF_MIME_TYPE, JPEG_MIME_TYPE, MP4_MIME_TYPE, PNG_MIME_TYPE, SUBRIP_MIME_TYPE, WEBP_MIME_TYPE = MIME_TYPES
15
+ MIME_TYPE_MAP = {"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
16
+ "png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
17
+
18
+ def media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
19
+ boundary: SecureRandom.hex)
20
+ validate!(file_path: file_path, media_category: media_category)
21
+ upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
22
+ upload_body = construct_upload_body(file_path, media_type, boundary)
23
+ upload_client.content_type = "multipart/form-data, boundary=#{boundary}"
24
+ upload_client.post("media/upload.json?media_category=#{media_category}", upload_body)
25
+ end
26
+
27
+ def chunked_media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
28
+ media_category), boundary: SecureRandom.hex, chunk_size_mb: 8)
29
+ validate!(file_path: file_path, media_category: media_category)
30
+ upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
31
+ media = init(upload_client, file_path, media_type, media_category)
32
+ chunk_size = chunk_size_mb * BYTES_PER_MB
33
+ chunk_paths = split(file_path, chunk_size)
34
+ append(upload_client, chunk_paths, media, media_type, boundary)
35
+ upload_client.post("media/upload.json?command=FINALIZE&media_id=#{media["media_id"]}")
36
+ end
37
+
38
+ def await_processing(client:, media:)
39
+ upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
40
+ loop do
41
+ status = upload_client.get("media/upload.json?command=STATUS&media_id=#{media["media_id"]}")
42
+ return status if status["processing_info"]["state"] == "succeeded"
43
+
44
+ sleep status["processing_info"]["check_after_secs"].to_i
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def validate!(file_path:, media_category:)
51
+ raise "File not found: #{file_path}" unless File.exist?(file_path)
52
+
53
+ return if MEDIA_CATEGORIES.include?(media_category.downcase)
54
+
55
+ raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
56
+ end
57
+
58
+ def infer_media_type(file_path, media_category)
59
+ case media_category.downcase
60
+ when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
61
+ when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
62
+ when SUBTITLES then SUBRIP_MIME_TYPE
63
+ else MIME_TYPE_MAP[File.extname(file_path).delete(".").downcase] || DEFAULT_MIME_TYPE
64
+ end
65
+ end
66
+
67
+ def init(upload_client, file_path, media_type, media_category)
68
+ total_bytes = File.size(file_path)
69
+ query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
70
+ upload_client.post("media/upload.json?#{query}")
71
+ end
72
+
73
+ def split(file_path, chunk_size)
74
+ file_number = -1
75
+
76
+ [].tap do |chunk_paths|
77
+ File.open(file_path, "rb") do |f|
78
+ while (chunk = f.read(chunk_size))
79
+ chunk_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
80
+ File.write(path, chunk)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def append(upload_client, chunk_paths, media, media_type, boundary = SecureRandom.hex)
88
+ threads = chunk_paths.map.with_index do |chunk_path, index|
89
+ Thread.new do
90
+ upload_body = construct_upload_body(chunk_path, media_type, boundary)
91
+ query = "command=APPEND&media_id=#{media["media_id"]}&segment_index=#{index}"
92
+ upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
93
+ end
94
+ end
95
+ threads.each(&:join)
96
+ end
97
+
98
+ def upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
99
+ # Initialize a new client to avoid shared connection issues
100
+ client = upload_client.dup
101
+ client.connection = Connection.new(**upload_client.connection.configuration.merge(base_url: "https://upload.twitter.com/1.1/"))
102
+ client.content_type = "multipart/form-data, boundary=#{boundary}"
103
+ client.post("media/upload.json?#{query}", upload_body)
104
+ rescue NetworkError, ServerError
105
+ retries ||= 0
106
+ ((retries += 1) < MAX_RETRIES) ? retry : raise
107
+ ensure
108
+ cleanup_chunk(chunk_path)
109
+ end
110
+
111
+ def cleanup_chunk(chunk_path)
112
+ dirname = File.dirname(chunk_path)
113
+ File.delete(chunk_path)
114
+ Dir.delete(dirname) if Dir.empty?(dirname)
115
+ end
116
+
117
+ def construct_upload_body(file_path, media_type, boundary = SecureRandom.hex)
118
+ "--#{boundary}\r\n" \
119
+ "Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
120
+ "Content-Type: #{media_type}\r\n\r\n" \
121
+ "#{File.read(file_path)}\r\n" \
122
+ "--#{boundary}--\r\n"
123
+ end
124
+ end
125
+ end
@@ -72,22 +72,18 @@ module X
72
72
  end
73
73
 
74
74
  def signature_base_string(method, url, params)
75
- "#{method}&#{encode(url)}&#{encode(encode_params(params))}"
76
- end
77
-
78
- def encode_params(params)
79
- params.sort.map { |k, v| "#{k}=#{encode(v.to_s)}" }.join("&")
75
+ "#{method}&#{escape(url)}&#{escape(URI.encode_www_form(params.sort))}"
80
76
  end
81
77
 
82
78
  def signing_key
83
- "#{encode(api_key_secret)}&#{encode(access_token_secret)}"
79
+ "#{escape(api_key_secret)}&#{escape(access_token_secret)}"
84
80
  end
85
81
 
86
82
  def format_oauth_header(params)
87
- "OAuth #{params.sort.map { |k, v| "#{k}=\"#{encode(v.to_s)}\"" }.join(", ")}"
83
+ "OAuth #{params.sort.map { |k, v| "#{k}=\"#{escape(v.to_s)}\"" }.join(", ")}"
88
84
  end
89
85
 
90
- def encode(value)
86
+ def escape(value)
91
87
  # TODO: Replace CGI.escape with CGI.escapeURIComponent when support for Ruby 3.1 is dropped
92
88
  CGI.escape(value.to_s).gsub("+", "%20")
93
89
  end
@@ -47,9 +47,7 @@ module X
47
47
  end
48
48
 
49
49
  def send_new_request(new_uri, new_request)
50
- @connection = Connection.new(base_url: new_uri, open_timeout: connection.open_timeout,
51
- read_timeout: connection.read_timeout, write_timeout: connection.write_timeout,
52
- debug_output: connection.debug_output, proxy_url: connection.proxy_uri)
50
+ @connection = Connection.new(**connection.configuration.merge(base_url: new_uri))
53
51
  connection.send_request(new_request)
54
52
  end
55
53
  end
@@ -12,7 +12,7 @@ module X
12
12
  delete: Net::HTTP::Delete
13
13
  }.freeze
14
14
  DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8".freeze
15
- DEFAULT_USER_AGENT = "X-Client/#{VERSION} Ruby/#{RUBY_VERSION}".freeze
15
+ DEFAULT_USER_AGENT = "X-Client/#{VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} (#{RUBY_PLATFORM})".freeze
16
16
  AUTHORIZATION_HEADER = "Authorization".freeze
17
17
  CONTENT_TYPE_HEADER = "Content-Type".freeze
18
18
  USER_AGENT_HEADER = "User-Agent".freeze
@@ -24,22 +24,30 @@ module X
24
24
  @user_agent = user_agent
25
25
  end
26
26
 
27
- def build(authenticator, http_method, url, body: nil)
28
- request = create_request(http_method, url, body)
27
+ def build(authenticator, http_method, uri, body: nil)
28
+ request = create_request(http_method, uri, body)
29
29
  add_authorization(request, authenticator)
30
30
  add_content_type(request)
31
31
  add_user_agent(request)
32
32
  request
33
33
  end
34
34
 
35
+ def configuration
36
+ {
37
+ content_type: content_type,
38
+ user_agent: user_agent
39
+ }
40
+ end
41
+
35
42
  private
36
43
 
37
- def create_request(http_method, url, body)
44
+ def create_request(http_method, uri, body)
38
45
  http_method_class = HTTP_METHODS[http_method]
39
46
 
40
47
  raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
41
48
 
42
- request = http_method_class.new(url)
49
+ escaped_uri = escape_query_params(uri)
50
+ request = http_method_class.new(escaped_uri)
43
51
  request.body = body if body && http_method != :get
44
52
  request
45
53
  end
@@ -55,5 +63,9 @@ module X
55
63
  def add_user_agent(request)
56
64
  request.add_field(USER_AGENT_HEADER, user_agent) if user_agent
57
65
  end
66
+
67
+ def escape_query_params(uri)
68
+ URI(uri).tap { |u| u.query = URI.encode_www_form(URI.decode_www_form(uri.query)) if uri.query }
69
+ end
58
70
  end
59
71
  end
@@ -1,12 +1,13 @@
1
1
  require "json"
2
2
  require "net/http"
3
- require_relative "errors/bad_request_error"
4
3
  require_relative "errors/authentication_error"
4
+ require_relative "errors/bad_request_error"
5
5
  require_relative "errors/forbidden_error"
6
6
  require_relative "errors/not_found_error"
7
- require_relative "errors/too_many_requests_error"
8
- require_relative "errors/server_error"
7
+ require_relative "errors/payload_too_large_error"
8
+ require_relative "errors/internal_server_error"
9
9
  require_relative "errors/service_unavailable_error"
10
+ require_relative "errors/too_many_requests_error"
10
11
 
11
12
  module X
12
13
  # Process HTTP responses
@@ -18,8 +19,9 @@ module X
18
19
  401 => AuthenticationError,
19
20
  403 => ForbiddenError,
20
21
  404 => NotFoundError,
22
+ 413 => PayloadTooLargeError,
21
23
  429 => TooManyRequestsError,
22
- 500 => ServerError,
24
+ 500 => InternalServerError,
23
25
  503 => ServiceUnavailableError
24
26
  }.freeze
25
27
  JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
@@ -32,19 +34,30 @@ module X
32
34
  end
33
35
 
34
36
  def handle(response)
35
- if successful_json_response?(response)
36
- return JSON.parse(response.body, array_class: array_class, object_class: object_class)
37
+ if success?(response)
38
+ JSON.parse(response.body, array_class: array_class, object_class: object_class) if json?(response)
39
+ else
40
+ error_class = ERROR_CLASSES[response.code.to_i] || Error
41
+ error_message = "#{response.code} #{response.message}"
42
+ raise error_class.new(error_message, response: response)
37
43
  end
44
+ end
38
45
 
39
- error_class = ERROR_CLASSES[response.code.to_i] || Error
40
- error_message = "#{response.code} #{response.message}"
41
- raise error_class.new(error_message, response: response)
46
+ def configuration
47
+ {
48
+ array_class: array_class,
49
+ object_class: object_class
50
+ }
42
51
  end
43
52
 
44
53
  private
45
54
 
46
- def successful_json_response?(response)
47
- response.is_a?(Net::HTTPSuccess) && response.body && JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
55
+ def success?(response)
56
+ response.is_a?(Net::HTTPSuccess)
57
+ end
58
+
59
+ def json?(response)
60
+ response.body && JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
48
61
  end
49
62
  end
50
63
  end
data/lib/x/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.9.0")
4
+ VERSION = Gem::Version.create("0.10.0")
5
5
  end
data/sig/x.rbs CHANGED
@@ -28,17 +28,16 @@ module X
28
28
  def generate_signature: (String method, String url, Hash[String, String] params) -> String
29
29
  def hmac_signature: (String base_string) -> String
30
30
  def signature_base_string: (String method, String url, Hash[String, String] params) -> String
31
- def encode_params: (Hash[String, String] params) -> String
32
31
  def signing_key: -> String
33
32
  def format_oauth_header: (Hash[String, String] params) -> String
34
- def encode: (String value) -> String
33
+ def escape: (String value) -> String
35
34
  end
36
35
 
37
36
  class Error < StandardError
38
37
  JSON_CONTENT_TYPE_REGEXP: Regexp
39
38
 
40
- attr_reader object: Hash[String, untyped]
41
- def initialize: (String msg, response: Net::HTTPResponse) -> void
39
+ attr_reader object: untyped
40
+ def initialize: (String msg, response: Net::HTTPResponse?) -> void
42
41
 
43
42
  private
44
43
  def json_response?: (Net::HTTPResponse response) -> bool
@@ -56,9 +55,15 @@ module X
56
55
  class ForbiddenError < ClientError
57
56
  end
58
57
 
58
+ class InternalServerError < ServerError
59
+ end
60
+
59
61
  class NotFoundError < ClientError
60
62
  end
61
63
 
64
+ class PayloadTooLargeError < ClientError
65
+ end
66
+
62
67
  class TooManyRedirectsError < ClientError
63
68
  end
64
69
 
@@ -88,7 +93,8 @@ module X
88
93
  DEFAULT_OPEN_TIMEOUT: Integer
89
94
  DEFAULT_READ_TIMEOUT: Integer
90
95
  DEFAULT_WRITE_TIMEOUT: Integer
91
- NETWORK_ERRORS: Array[(singleton(::Errno::ECONNREFUSED) | singleton(::Net::OpenTimeout) | singleton(::Net::ReadTimeout))]
96
+ NETWORK_ERRORS: Array[(singleton(Errno::ECONNREFUSED) | singleton(Errno::ECONNRESET) | singleton(Net::OpenTimeout) | singleton(Net::ReadTimeout) | singleton(OpenSSL::SSL::SSLError))]
97
+
92
98
  extend Forwardable
93
99
  @http_client: Net::HTTP
94
100
 
@@ -101,6 +107,7 @@ module X
101
107
  def send_request: (Net::HTTPRequest request) -> Net::HTTPResponse
102
108
  def base_uri=: (URI::Generic | String base_url) -> void
103
109
  def debug_output: -> IO?
110
+ def configuration: -> Hash[Symbol, untyped]
104
111
 
105
112
  private
106
113
  def apply_http_client_settings: (open_timeout: Float | Integer, read_timeout: Float | Integer, write_timeout: Float | Integer, debug_output: IO?) -> untyped
@@ -121,13 +128,15 @@ module X
121
128
  attr_accessor content_type: String
122
129
  attr_accessor user_agent: String
123
130
  def initialize: (?content_type: String, ?user_agent: String) -> void
124
- def build: (BearerTokenAuthenticator | OauthAuthenticator authenticator, Symbol http_method, URI::Generic url, ?body: String?) -> (Net::HTTPRequest)
131
+ def build: (BearerTokenAuthenticator | OauthAuthenticator authenticator, Symbol http_method, URI::Generic uri, ?body: String?) -> (Net::HTTPRequest)
132
+ def configuration: -> Hash[Symbol, untyped]
125
133
 
126
134
  private
127
- def create_request: (Symbol http_method, URI::Generic url, String? body) -> (Net::HTTPRequest)
135
+ def create_request: (Symbol http_method, URI::Generic uri, String? body) -> (Net::HTTPRequest)
128
136
  def add_authorization: (Net::HTTPRequest request, BearerTokenAuthenticator | OauthAuthenticator authenticator) -> void
129
137
  def add_content_type: (Net::HTTPRequest request) -> void
130
138
  def add_user_agent: (Net::HTTPRequest request) -> void
139
+ def escape_query_params: (URI::Generic uri) -> URI::Generic
131
140
  end
132
141
 
133
142
  class RedirectHandler
@@ -149,16 +158,18 @@ module X
149
158
  class ResponseHandler
150
159
  DEFAULT_ARRAY_CLASS: Class
151
160
  DEFAULT_OBJECT_CLASS: Class
152
- ERROR_CLASSES: Hash[Integer, singleton(AuthenticationError) | singleton(BadRequestError) | singleton(ForbiddenError) | singleton(NotFoundError) | singleton(ServerError) | singleton(ServiceUnavailableError) | singleton(TooManyRequestsError)]
161
+ ERROR_CLASSES: Hash[Integer, singleton(AuthenticationError) | singleton(BadRequestError) | singleton(ForbiddenError) | singleton(InternalServerError) | singleton(NotFoundError) | singleton(PayloadTooLargeError) | singleton(ServiceUnavailableError) | singleton(TooManyRequestsError)]
153
162
  JSON_CONTENT_TYPE_REGEXP: Regexp
154
163
 
155
164
  attr_accessor array_class: Class
156
165
  attr_accessor object_class: Class
157
166
  def initialize: (?array_class: Class, ?object_class: Class) -> void
158
- def handle: (Net::HTTPResponse response) -> Hash[String, untyped]
167
+ def handle: (Net::HTTPResponse response) -> untyped
168
+ def configuration: -> Hash[Symbol, Class]
159
169
 
160
170
  private
161
- def successful_json_response?: (Net::HTTPResponse response) -> bool
171
+ def success?: (Net::HTTPResponse response) -> bool
172
+ def json?: (Net::HTTPResponse response) -> bool
162
173
  end
163
174
 
164
175
  class Client
@@ -169,15 +180,80 @@ module X
169
180
  @redirect_handler: RedirectHandler
170
181
  @response_handler: ResponseHandler
171
182
 
172
- attr_reader base_uri: URI::Generic
183
+ attr_accessor access_token: String
184
+ attr_accessor access_token_secret: String
185
+ attr_accessor api_key: String
186
+ attr_accessor api_key_secret: String
187
+ attr_accessor bearer_token: String
188
+ attr_accessor base_uri: URI::Generic | String
189
+ attr_accessor open_timeout: Float | Integer
190
+ attr_accessor read_timeout: Float | Integer
191
+ attr_accessor write_timeout: Float | Integer
192
+ attr_accessor proxy_url: String
193
+ attr_accessor content_type: String
194
+ attr_accessor user_agent: String
195
+ attr_accessor debug_output: IO?
196
+ attr_accessor array_class: Class
197
+ attr_accessor object_class: Class
198
+ attr_accessor max_redirects: Integer
199
+ attr_accessor authenticator: BearerTokenAuthenticator | OauthAuthenticator
200
+ attr_accessor connection: Connection
201
+ attr_accessor request_builder: RequestBuilder
202
+ attr_accessor redirect_handler: RedirectHandler
203
+ attr_accessor response_handler: ResponseHandler
204
+ alias base_url base_uri
205
+
206
+
173
207
  def initialize: (?bearer_token: String?, ?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?base_url: URI::Generic | String, ?content_type: String, ?user_agent: String, ?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO?, ?array_class: Class, ?object_class: Class, ?max_redirects: Integer) -> void
174
- def get: (String endpoint) -> Hash[String, untyped]
175
- def post: (String endpoint, ?nil body) -> Hash[String, untyped]
176
- def put: (String endpoint, ?nil body) -> Hash[String, untyped]
177
- def delete: (String endpoint) -> Hash[String, untyped]
208
+ def get: (String endpoint) -> untyped
209
+ def post: (String endpoint, ?String? body) -> untyped
210
+ def put: (String endpoint, ?String? body) -> untyped
211
+ def delete: (String endpoint) -> untyped
178
212
 
179
213
  private
180
214
  def initialize_authenticator: (String? bearer_token, String? api_key, String? api_key_secret, String? access_token, String? access_token_secret) -> (BearerTokenAuthenticator | OauthAuthenticator)
181
- def send_request: (Symbol http_method, String endpoint, ?nil body) -> Hash[String, untyped]
215
+ def send_request: (Symbol http_method, String endpoint, ?String? body) -> untyped
182
216
  end
217
+
218
+ module MediaUpload
219
+ MAX_RETRIES: Integer
220
+ BYTES_PER_MB: Integer
221
+ MEDIA_CATEGORIES: Array[String]
222
+ DM_GIF: String
223
+ DM_IMAGE: String
224
+ DM_VIDEO: String
225
+ SUBTITLES: String
226
+ TWEET_GIF: String
227
+ TWEET_IMAGE: String
228
+ TWEET_VIDEO: String
229
+ DEFAULT_MIME_TYPE: String
230
+ MIME_TYPES: Array[String]
231
+ GIF_MIME_TYPE: String
232
+ JPEG_MIME_TYPE: String
233
+ MP4_MIME_TYPE: String
234
+ PNG_MIME_TYPE: String
235
+ SUBRIP_MIME_TYPE: String
236
+ WEBP_MIME_TYPE: String
237
+ MIME_TYPE_MAP: Hash[String, String]
238
+ extend MediaUpload
239
+
240
+ def media_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String) -> untyped
241
+ def chunked_media_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String, ?chunk_size_mb: Integer) -> untyped
242
+ def await_processing: (client: Client, media: untyped) -> untyped
243
+
244
+ private
245
+ def validate!: (file_path: String, media_category: String) -> nil
246
+ def infer_media_type: (String file_path, String media_category) -> String
247
+ def init: (Client upload_client, String file_path, String media_type, String media_category) -> untyped
248
+ def split: (String file_path, Integer chunk_size) -> Array[String]
249
+ def append: (Client upload_client, Array[String] chunk_paths, untyped media, String media_type, ?String boundary) -> Array[String]
250
+ def upload_chunk: (Client upload_client, String query, String chunk_path, String media_type, String boundary) -> Integer?
251
+ def cleanup_chunk: (String chunk_path) -> Integer?
252
+ def finalize: (Client upload_client, untyped media) -> untyped
253
+ def construct_upload_body: (String file_path, String media_type, ?String boundary) -> String
254
+ end
255
+ end
256
+
257
+ class Dir
258
+ def self.mktmpdir: (?String? prefix_suffix) -> String
183
259
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-26 00:00:00.000000000 Z
11
+ date: 2023-10-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -31,12 +31,15 @@ files:
31
31
  - lib/x/errors/client_error.rb
32
32
  - lib/x/errors/error.rb
33
33
  - lib/x/errors/forbidden_error.rb
34
+ - lib/x/errors/internal_server_error.rb
34
35
  - lib/x/errors/network_error.rb
35
36
  - lib/x/errors/not_found_error.rb
37
+ - lib/x/errors/payload_too_large_error.rb
36
38
  - lib/x/errors/server_error.rb
37
39
  - lib/x/errors/service_unavailable_error.rb
38
40
  - lib/x/errors/too_many_redirects_error.rb
39
41
  - lib/x/errors/too_many_requests_error.rb
42
+ - lib/x/media_upload.rb
40
43
  - lib/x/oauth_authenticator.rb
41
44
  - lib/x/redirect_handler.rb
42
45
  - lib/x/request_builder.rb
@@ -69,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
72
  - !ruby/object:Gem::Version
70
73
  version: '0'
71
74
  requirements: []
72
- rubygems_version: 3.4.19
75
+ rubygems_version: 3.4.20
73
76
  signing_key:
74
77
  specification_version: 4
75
78
  summary: A Ruby interface to the X API.