x 0.17.0 → 0.18.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +4 -4
  4. data/lib/x/account_uploader.rb +168 -0
  5. data/lib/x/authenticator.rb +12 -0
  6. data/lib/x/bearer_token_authenticator.rb +22 -1
  7. data/lib/x/client.rb +95 -57
  8. data/lib/x/client_credentials.rb +208 -0
  9. data/lib/x/connection.rb +88 -2
  10. data/lib/x/errors/bad_gateway.rb +1 -0
  11. data/lib/x/errors/bad_request.rb +1 -0
  12. data/lib/x/errors/client_error.rb +1 -0
  13. data/lib/x/errors/connection_exception.rb +1 -0
  14. data/lib/x/errors/error.rb +1 -0
  15. data/lib/x/errors/forbidden.rb +1 -0
  16. data/lib/x/errors/gateway_timeout.rb +1 -0
  17. data/lib/x/errors/gone.rb +1 -0
  18. data/lib/x/errors/http_error.rb +47 -4
  19. data/lib/x/errors/internal_server_error.rb +1 -0
  20. data/lib/x/errors/invalid_media_type.rb +6 -0
  21. data/lib/x/errors/network_error.rb +1 -0
  22. data/lib/x/errors/not_acceptable.rb +1 -0
  23. data/lib/x/errors/not_found.rb +1 -0
  24. data/lib/x/errors/payload_too_large.rb +1 -0
  25. data/lib/x/errors/server_error.rb +1 -0
  26. data/lib/x/errors/service_unavailable.rb +1 -0
  27. data/lib/x/errors/too_many_redirects.rb +1 -0
  28. data/lib/x/errors/too_many_requests.rb +32 -0
  29. data/lib/x/errors/unauthorized.rb +1 -0
  30. data/lib/x/errors/unprocessable_entity.rb +1 -0
  31. data/lib/x/media_upload_validator.rb +19 -0
  32. data/lib/x/media_uploader.rb +117 -5
  33. data/lib/x/oauth2_authenticator.rb +169 -0
  34. data/lib/x/oauth_authenticator.rb +99 -2
  35. data/lib/x/rate_limit.rb +57 -1
  36. data/lib/x/redirect_handler.rb +55 -1
  37. data/lib/x/request_builder.rb +36 -0
  38. data/lib/x/response_parser.rb +21 -0
  39. data/lib/x/version.rb +2 -1
  40. data/sig/x.rbs +78 -17
  41. metadata +6 -2
@@ -6,12 +6,42 @@ require_relative "errors/too_many_redirects"
6
6
  require_relative "request_builder"
7
7
 
8
8
  module X
9
+ # Handles HTTP redirects for API requests
10
+ # @api public
9
11
  class RedirectHandler
12
+ # Default maximum number of redirects to follow
10
13
  DEFAULT_MAX_REDIRECTS = 10
11
14
 
15
+ # The maximum number of redirects to follow
16
+ # @api public
17
+ # @return [Integer] the maximum number of redirects to follow
18
+ # @example Get or set the maximum redirects
19
+ # handler.max_redirects = 5
12
20
  attr_accessor :max_redirects
13
- attr_reader :connection, :request_builder
14
21
 
22
+ # The connection for making requests
23
+ # @api public
24
+ # @return [Connection] the connection for making requests
25
+ # @example Get the connection
26
+ # handler.connection
27
+ attr_reader :connection
28
+
29
+ # The request builder for creating requests
30
+ # @api public
31
+ # @return [RequestBuilder] the request builder for creating requests
32
+ # @example Get the request builder
33
+ # handler.request_builder
34
+ attr_reader :request_builder
35
+
36
+ # Initialize a new RedirectHandler
37
+ #
38
+ # @api public
39
+ # @param connection [Connection] the connection for making requests
40
+ # @param request_builder [RequestBuilder] the request builder for creating requests
41
+ # @param max_redirects [Integer] the maximum number of redirects to follow
42
+ # @return [RedirectHandler] a new instance
43
+ # @example Create a redirect handler
44
+ # handler = X::RedirectHandler.new(connection: conn, request_builder: builder)
15
45
  def initialize(connection: Connection.new, request_builder: RequestBuilder.new,
16
46
  max_redirects: DEFAULT_MAX_REDIRECTS)
17
47
  @connection = connection
@@ -19,6 +49,18 @@ module X
19
49
  @max_redirects = max_redirects
20
50
  end
21
51
 
52
+ # Handle redirects for an HTTP response
53
+ #
54
+ # @api public
55
+ # @param response [Net::HTTPResponse] the HTTP response to handle
56
+ # @param request [Net::HTTPRequest] the original HTTP request
57
+ # @param base_url [String] the base URL for the request
58
+ # @param authenticator [Authenticator] the authenticator for requests
59
+ # @param redirect_count [Integer] the current redirect count
60
+ # @return [Net::HTTPResponse] the final HTTP response after following redirects
61
+ # @raise [TooManyRedirects] if the maximum number of redirects is exceeded
62
+ # @example Handle a response
63
+ # response = handler.handle(response: resp, request: req, base_url: url)
22
64
  def handle(response:, request:, base_url:, authenticator: Authenticator.new, redirect_count: 0)
23
65
  if response.is_a?(Net::HTTPRedirection)
24
66
  raise TooManyRedirects, "Too many redirects" if redirect_count > max_redirects
@@ -36,12 +78,24 @@ module X
36
78
 
37
79
  private
38
80
 
81
+ # Build a new URI from the redirect response
82
+ # @api private
83
+ # @param response [Net::HTTPResponse] the redirect response
84
+ # @param base_url [String] the base URL
85
+ # @return [URI] the new URI
39
86
  def build_new_uri(response, base_url)
40
87
  location = response.fetch("location")
41
88
  # If location is relative, it will join with the original base URL, otherwise it will overwrite it
42
89
  URI.join(base_url, location)
43
90
  end
44
91
 
92
+ # Build a new request for the redirect
93
+ # @api private
94
+ # @param request [Net::HTTPRequest] the original request
95
+ # @param uri [URI] the new URI
96
+ # @param response_code [Integer] the HTTP response code
97
+ # @param authenticator [Authenticator] the authenticator
98
+ # @return [Net::HTTPRequest] the new request
45
99
  def build_request(request, uri, response_code, authenticator)
46
100
  http_method = :get
47
101
  if [307, 308].include?(response_code)
@@ -4,11 +4,15 @@ require_relative "authenticator"
4
4
  require_relative "version"
5
5
 
6
6
  module X
7
+ # Builds HTTP requests for the X API
8
+ # @api public
7
9
  class RequestBuilder
10
+ # Default headers for API requests
8
11
  DEFAULT_HEADERS = {
9
12
  "Content-Type" => "application/json; charset=utf-8",
10
13
  "User-Agent" => "X-Client/#{VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
11
14
  }.freeze
15
+ # Mapping of HTTP method symbols to Net::HTTP classes
12
16
  HTTP_METHODS = {
13
17
  get: Net::HTTP::Get,
14
18
  post: Net::HTTP::Post,
@@ -16,6 +20,18 @@ module X
16
20
  delete: Net::HTTP::Delete
17
21
  }.freeze
18
22
 
23
+ # Build an HTTP request
24
+ #
25
+ # @api public
26
+ # @param http_method [Symbol] the HTTP method (:get, :post, :put, :delete)
27
+ # @param uri [URI] the request URI
28
+ # @param body [String, nil] the request body
29
+ # @param headers [Hash] additional headers for the request
30
+ # @param authenticator [Authenticator] the authenticator for the request
31
+ # @return [Net::HTTPRequest] the built HTTP request
32
+ # @raise [ArgumentError] if the HTTP method is not supported
33
+ # @example Build a GET request
34
+ # builder.build(http_method: :get, uri: URI("https://api.x.com/2/users/me"))
19
35
  def build(http_method:, uri:, body: nil, headers: {}, authenticator: Authenticator.new)
20
36
  request = create_request(http_method:, uri:, body:)
21
37
  add_headers(request:, headers:)
@@ -25,6 +41,12 @@ module X
25
41
 
26
42
  private
27
43
 
44
+ # Create an HTTP request
45
+ # @api private
46
+ # @param http_method [Symbol] the HTTP method
47
+ # @param uri [URI] the request URI
48
+ # @param body [String, nil] the request body
49
+ # @return [Net::HTTPRequest] the created request
28
50
  def create_request(http_method:, uri:, body:)
29
51
  http_method_class = HTTP_METHODS[http_method]
30
52
 
@@ -36,18 +58,32 @@ module X
36
58
  request
37
59
  end
38
60
 
61
+ # Add authentication to a request
62
+ # @api private
63
+ # @param request [Net::HTTPRequest] the request
64
+ # @param authenticator [Authenticator] the authenticator
65
+ # @return [void]
39
66
  def add_authentication(request:, authenticator:)
40
67
  authenticator.header(request).each do |key, value|
41
68
  request[key] = value
42
69
  end
43
70
  end
44
71
 
72
+ # Add headers to a request
73
+ # @api private
74
+ # @param request [Net::HTTPRequest] the request
75
+ # @param headers [Hash] additional headers
76
+ # @return [void]
45
77
  def add_headers(request:, headers:)
46
78
  DEFAULT_HEADERS.merge(headers).each do |key, value|
47
79
  request[key] = value
48
80
  end
49
81
  end
50
82
 
83
+ # Escape query parameters in a URI
84
+ # @api private
85
+ # @param uri [URI] the URI
86
+ # @return [URI] the URI with escaped query parameters
51
87
  def escape_query_params(uri)
52
88
  URI(uri).tap do |u|
53
89
  u.query = URI.encode_www_form(URI.decode_www_form(u.query)).gsub("%2C", ",") if u.query
@@ -17,7 +17,10 @@ require_relative "errors/unauthorized"
17
17
  require_relative "errors/unprocessable_entity"
18
18
 
19
19
  module X
20
+ # Parses HTTP responses from the X API
21
+ # @api public
20
22
  class ResponseParser
23
+ # Mapping of HTTP status codes to error classes
21
24
  ERROR_MAP = {
22
25
  400 => BadRequest,
23
26
  401 => Unauthorized,
@@ -35,6 +38,16 @@ module X
35
38
  504 => GatewayTimeout
36
39
  }.freeze
37
40
 
41
+ # Parse an HTTP response
42
+ #
43
+ # @api public
44
+ # @param response [Net::HTTPResponse] the HTTP response to parse
45
+ # @param array_class [Class, nil] the class for parsing JSON arrays
46
+ # @param object_class [Class, nil] the class for parsing JSON objects
47
+ # @return [Hash, Array, nil] the parsed response body
48
+ # @raise [HTTPError] if the response is not successful
49
+ # @example Parse a response
50
+ # parser.parse(response: response)
38
51
  def parse(response:, array_class: nil, object_class: nil)
39
52
  raise error(response) unless response.is_a?(Net::HTTPSuccess)
40
53
 
@@ -49,10 +62,18 @@ module X
49
62
 
50
63
  private
51
64
 
65
+ # Create an error from a response
66
+ # @api private
67
+ # @param response [Net::HTTPResponse] the HTTP response
68
+ # @return [HTTPError] the error
52
69
  def error(response)
53
70
  error_class(response).new(response:)
54
71
  end
55
72
 
73
+ # Get the error class for a response
74
+ # @api private
75
+ # @param response [Net::HTTPResponse] the HTTP response
76
+ # @return [Class] the error class
56
77
  def error_class(response)
57
78
  ERROR_MAP[Integer(response.code)] || HTTPError
58
79
  end
data/lib/x/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.17.0")
4
+ # The current version of the X gem
5
+ VERSION = Gem::Version.create("0.18.0")
5
6
  end
data/sig/x.rbs CHANGED
@@ -101,6 +101,9 @@ module X
101
101
  class TooManyRedirects < Error
102
102
  end
103
103
 
104
+ class InvalidMediaType < Error
105
+ end
106
+
104
107
  class TooManyRequests < ClientError
105
108
  @rate_limits: Array[RateLimit]
106
109
 
@@ -205,12 +208,64 @@ module X
205
208
  def json?: (Net::HTTPResponse response) -> bool
206
209
  end
207
210
 
211
+ class OAuth2Authenticator < Authenticator
212
+ TOKEN_PATH: String
213
+ TOKEN_HOST: String
214
+ REFRESH_GRANT_TYPE: String
215
+ EXPIRATION_BUFFER: Integer
216
+
217
+ attr_accessor client_id: String
218
+ attr_accessor client_secret: String
219
+ attr_accessor access_token: String
220
+ attr_accessor refresh_token: String
221
+ attr_accessor expires_at: Time?
222
+ attr_accessor connection: Connection
223
+ def initialize: (client_id: String, client_secret: String, access_token: String, refresh_token: String, ?expires_at: Time?, ?connection: Connection) -> void
224
+ def header: (Net::HTTPRequest? request) -> Hash[String, String]
225
+ def token_expired?: -> bool
226
+ def refresh_token!: -> Hash[String, untyped]
227
+
228
+ private
229
+ def send_token_request: -> Net::HTTPResponse
230
+ def build_token_request: -> Net::HTTP::Post
231
+ def handle_token_response: (Net::HTTPResponse response) -> Hash[String, untyped]
232
+ def update_tokens: (Hash[String, untyped] token_response) -> void
233
+ end
234
+
235
+ module ClientCredentials
236
+ attr_reader api_key: String?
237
+ attr_reader api_key_secret: String?
238
+ attr_reader access_token: String?
239
+ attr_reader access_token_secret: String?
240
+ attr_reader bearer_token: String?
241
+ attr_reader client_id: String?
242
+ attr_reader client_secret: String?
243
+ attr_reader refresh_token: String?
244
+ def api_key=: (String api_key) -> void
245
+ def api_key_secret=: (String api_key_secret) -> void
246
+ def access_token=: (String access_token) -> void
247
+ def access_token_secret=: (String access_token_secret) -> void
248
+ def bearer_token=: (String bearer_token) -> void
249
+ def client_id=: (String client_id) -> void
250
+ def client_secret=: (String client_secret) -> void
251
+ def refresh_token=: (String refresh_token) -> void
252
+
253
+ private
254
+ def initialize_credentials: (api_key: String?, api_key_secret: String?, access_token: String?, access_token_secret: String?, bearer_token: String?, client_id: String?, client_secret: String?, refresh_token: String?) -> void
255
+ def initialize_authenticator: -> (Authenticator | BearerTokenAuthenticator | OAuthAuthenticator | OAuth2Authenticator)
256
+ def oauth_authenticator: -> OAuthAuthenticator?
257
+ def oauth2_authenticator: -> OAuth2Authenticator?
258
+ def bearer_authenticator: -> BearerTokenAuthenticator?
259
+ end
260
+
208
261
  class Client
262
+ include ClientCredentials
263
+
209
264
  DEFAULT_BASE_URL: String
210
265
  DEFAULT_ARRAY_CLASS: singleton(Array)
211
266
  DEFAULT_OBJECT_CLASS: singleton(Hash)
212
267
  extend Forwardable
213
- @authenticator: Authenticator | BearerTokenAuthenticator | OAuthAuthenticator
268
+ @authenticator: Authenticator | BearerTokenAuthenticator | OAuthAuthenticator | OAuth2Authenticator
214
269
  @connection: Connection
215
270
  @request_builder: RequestBuilder
216
271
  @redirect_handler: RedirectHandler
@@ -219,26 +274,14 @@ module X
219
274
  attr_accessor base_url: String
220
275
  attr_accessor default_array_class: singleton(Array)
221
276
  attr_accessor default_object_class: singleton(Hash)
222
- attr_reader api_key: String?
223
- attr_reader api_key_secret: String?
224
- attr_reader access_token: String?
225
- attr_reader access_token_secret: String?
226
- attr_reader bearer_token: String?
227
- def initialize: (?api_key: nil, ?api_key_secret: nil, ?access_token: nil, ?access_token_secret: nil, ?bearer_token: nil, ?base_url: String, ?open_timeout: Integer, ?read_timeout: Integer, ?write_timeout: Integer, ?debug_output: untyped, ?proxy_url: nil, ?default_array_class: singleton(Array), ?default_object_class: singleton(Hash), ?max_redirects: Integer) -> void
277
+ attr_reader authenticator: Authenticator | BearerTokenAuthenticator | OAuthAuthenticator | OAuth2Authenticator
278
+ def initialize: (?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?bearer_token: String?, ?client_id: String?, ?client_secret: String?, ?refresh_token: String?, ?base_url: String, ?open_timeout: Integer, ?read_timeout: Integer, ?write_timeout: Integer, ?debug_output: untyped, ?proxy_url: String?, ?default_array_class: singleton(Array), ?default_object_class: singleton(Hash), ?max_redirects: Integer) -> void
228
279
  def get: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
229
280
  def post: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
230
281
  def put: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
231
282
  def delete: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
232
- def api_key=: (String api_key) -> void
233
- def api_key_secret=: (String api_key_secret) -> void
234
- def access_token=: (String access_token) -> void
235
- def access_token_secret=: (String access_token_secret) -> void
236
- def bearer_token=: (String bearer_token) -> void
237
283
 
238
284
  private
239
- def initialize_oauth: (String? api_key, String? api_key_secret, String? access_token, String? access_token_secret, String? bearer_token) -> void
240
- def initialize_default_classes: (singleton(Array) default_array_class, singleton(Hash) default_object_class) -> singleton(Hash)
241
- def initialize_authenticator: -> (Authenticator | BearerTokenAuthenticator | OAuthAuthenticator)
242
285
  def execute_request: (:delete | :get | :post | :put http_method, String endpoint, ?body: String?, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> nil
243
286
  end
244
287
 
@@ -252,7 +295,6 @@ module X
252
295
  TWEET_GIF: String
253
296
  TWEET_IMAGE: String
254
297
  TWEET_VIDEO: String
255
- DEFAULT_MIME_TYPE: String
256
298
  MIME_TYPES: Array[String]
257
299
  GIF_MIME_TYPE: String
258
300
  JPEG_MIME_TYPE: String
@@ -269,9 +311,9 @@ module X
269
311
  def chunked_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String, ?chunk_size_mb: Integer) -> untyped
270
312
  def await_processing: (client: Client, media: untyped) -> untyped
271
313
  def await_processing!: (client: Client, media: untyped) -> untyped
314
+ def infer_media_type: (String file_path, String media_category) -> String
272
315
 
273
316
  private
274
- def infer_media_type: (String file_path, String media_category) -> String
275
317
  def split: (String file_path, Integer chunk_size) -> Array[String]
276
318
  def init: (client: Client, file_path: String, media_type: String, media_category: String) -> untyped
277
319
  def append: (client: Client, file_paths: Array[String], media: untyped, ?boundary: String) -> Array[String]
@@ -288,4 +330,23 @@ module X
288
330
  def validate_file_path!: (file_path: String) -> nil
289
331
  def validate_media_category!: (media_category: String) -> nil
290
332
  end
333
+
334
+ module AccountUploader
335
+ V1_BASE_URL: String
336
+ SUPPORTED_EXTENSIONS: Array[String]
337
+ MIME_TYPE_MAP: Hash[String, String]
338
+ extend AccountUploader
339
+
340
+ def update_profile_image: (client: Client, file_path: String, ?boundary: String) -> untyped
341
+ def upload_profile_image_binary: (client: Client, content: String, ?boundary: String) -> untyped
342
+ def update_profile_banner: (client: Client, file_path: String, ?width: Integer?, ?height: Integer?, ?offset_left: Integer?, ?offset_top: Integer?, ?boundary: String) -> untyped
343
+ def upload_profile_banner_binary: (client: Client, content: String, ?width: Integer?, ?height: Integer?, ?offset_left: Integer?, ?offset_top: Integer?, ?boundary: String) -> untyped
344
+
345
+ private
346
+ def v1_client: (Client client) -> Client
347
+ def validate_file!: (String file_path) -> nil
348
+ def construct_multipart_body: (field_name: String, content: String, boundary: String) -> String
349
+ def construct_banner_body: (content: String, width: Integer?, height: Integer?, offset_left: Integer?, offset_top: Integer?, boundary: String) -> String
350
+ def multipart_field: (String name, untyped value, String boundary) -> String
351
+ end
291
352
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -35,9 +35,11 @@ files:
35
35
  - bin/console
36
36
  - bin/setup
37
37
  - lib/x.rb
38
+ - lib/x/account_uploader.rb
38
39
  - lib/x/authenticator.rb
39
40
  - lib/x/bearer_token_authenticator.rb
40
41
  - lib/x/client.rb
42
+ - lib/x/client_credentials.rb
41
43
  - lib/x/connection.rb
42
44
  - lib/x/errors/bad_gateway.rb
43
45
  - lib/x/errors/bad_request.rb
@@ -49,6 +51,7 @@ files:
49
51
  - lib/x/errors/gone.rb
50
52
  - lib/x/errors/http_error.rb
51
53
  - lib/x/errors/internal_server_error.rb
54
+ - lib/x/errors/invalid_media_type.rb
52
55
  - lib/x/errors/network_error.rb
53
56
  - lib/x/errors/not_acceptable.rb
54
57
  - lib/x/errors/not_found.rb
@@ -61,6 +64,7 @@ files:
61
64
  - lib/x/errors/unprocessable_entity.rb
62
65
  - lib/x/media_upload_validator.rb
63
66
  - lib/x/media_uploader.rb
67
+ - lib/x/oauth2_authenticator.rb
64
68
  - lib/x/oauth_authenticator.rb
65
69
  - lib/x/rate_limit.rb
66
70
  - lib/x/redirect_handler.rb
@@ -94,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
98
  - !ruby/object:Gem::Version
95
99
  version: '0'
96
100
  requirements: []
97
- rubygems_version: 3.6.9
101
+ rubygems_version: 4.0.3
98
102
  specification_version: 4
99
103
  summary: A Ruby interface to the X API.
100
104
  test_files: []