x 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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.