x 0.19.0 → 1.0.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +648 -15
  4. data/README.md +506 -73
  5. data/SECURITY.md +66 -0
  6. data/UPGRADING.md +670 -0
  7. data/lib/x/version.rb +16 -1
  8. data/lib/x.rb +13 -1
  9. data/sig/manifest.yaml +5 -0
  10. data/sig/x.rbs +5 -361
  11. metadata +57 -49
  12. data/bin/console +0 -10
  13. data/bin/setup +0 -6
  14. data/lib/x/account_uploader.rb +0 -168
  15. data/lib/x/authenticator.rb +0 -21
  16. data/lib/x/bearer_token_authenticator.rb +0 -36
  17. data/lib/x/client.rb +0 -191
  18. data/lib/x/client_credentials.rb +0 -208
  19. data/lib/x/connection.rb +0 -183
  20. data/lib/x/errors/bad_gateway.rb +0 -6
  21. data/lib/x/errors/bad_request.rb +0 -6
  22. data/lib/x/errors/client_error.rb +0 -6
  23. data/lib/x/errors/connection_exception.rb +0 -6
  24. data/lib/x/errors/error.rb +0 -4
  25. data/lib/x/errors/forbidden.rb +0 -6
  26. data/lib/x/errors/gateway_timeout.rb +0 -6
  27. data/lib/x/errors/gone.rb +0 -6
  28. data/lib/x/errors/http_error.rb +0 -84
  29. data/lib/x/errors/internal_server_error.rb +0 -6
  30. data/lib/x/errors/invalid_media_type.rb +0 -6
  31. data/lib/x/errors/network_error.rb +0 -6
  32. data/lib/x/errors/not_acceptable.rb +0 -6
  33. data/lib/x/errors/not_found.rb +0 -6
  34. data/lib/x/errors/payload_too_large.rb +0 -6
  35. data/lib/x/errors/server_error.rb +0 -6
  36. data/lib/x/errors/service_unavailable.rb +0 -6
  37. data/lib/x/errors/too_many_redirects.rb +0 -6
  38. data/lib/x/errors/too_many_requests.rb +0 -58
  39. data/lib/x/errors/unauthorized.rb +0 -6
  40. data/lib/x/errors/unprocessable_entity.rb +0 -6
  41. data/lib/x/media_upload_validator.rb +0 -36
  42. data/lib/x/media_uploader.rb +0 -234
  43. data/lib/x/oauth2_authenticator.rb +0 -169
  44. data/lib/x/oauth_authenticator.rb +0 -182
  45. data/lib/x/rate_limit.rb +0 -89
  46. data/lib/x/redirect_handler.rb +0 -109
  47. data/lib/x/request_builder.rb +0 -93
  48. data/lib/x/response_parser.rb +0 -81
  49. data/lib/x/stream_parser.rb +0 -75
data/lib/x/connection.rb DELETED
@@ -1,183 +0,0 @@
1
- require "forwardable"
2
- require "net/http"
3
- require "openssl"
4
- require "uri"
5
- require_relative "errors/network_error"
6
-
7
- module X
8
- # Manages HTTP connections to the X API
9
- # @api public
10
- class Connection
11
- extend Forwardable
12
-
13
- # Default host for the X API
14
- DEFAULT_HOST = "api.twitter.com".freeze
15
- # Default port for HTTPS connections
16
- DEFAULT_PORT = 443
17
- # Default timeout for opening connections in seconds
18
- DEFAULT_OPEN_TIMEOUT = 60 # seconds
19
- # Default timeout for reading responses in seconds
20
- DEFAULT_READ_TIMEOUT = 60 # seconds
21
- # Default timeout for writing requests in seconds
22
- DEFAULT_WRITE_TIMEOUT = 60 # seconds
23
- # Network errors that should be wrapped in NetworkError
24
- NETWORK_ERRORS = [
25
- Errno::ECONNREFUSED,
26
- Errno::ECONNRESET,
27
- Net::OpenTimeout,
28
- Net::ReadTimeout,
29
- OpenSSL::SSL::SSLError
30
- ].freeze
31
-
32
- # The timeout for opening connections in seconds
33
- # @api public
34
- # @return [Integer] the timeout for opening connections in seconds
35
- # @example Get or set the open timeout
36
- # connection.open_timeout = 30
37
- attr_accessor :open_timeout
38
-
39
- # The timeout for reading responses in seconds
40
- # @api public
41
- # @return [Integer] the timeout for reading responses in seconds
42
- # @example Get or set the read timeout
43
- # connection.read_timeout = 30
44
- attr_accessor :read_timeout
45
-
46
- # The timeout for writing requests in seconds
47
- # @api public
48
- # @return [Integer] the timeout for writing requests in seconds
49
- # @example Get or set the write timeout
50
- # connection.write_timeout = 30
51
- attr_accessor :write_timeout
52
-
53
- # The IO object for debug output
54
- # @api public
55
- # @return [IO] the IO object for debug output
56
- # @example Get or set the debug output
57
- # connection.debug_output = $stderr
58
- attr_accessor :debug_output
59
-
60
- # The proxy URL for requests
61
- # @api public
62
- # @return [String, nil] the proxy URL for requests
63
- # @example Get the proxy URL
64
- # connection.proxy_url
65
- attr_reader :proxy_url
66
-
67
- # The parsed proxy URI
68
- # @api public
69
- # @return [URI, nil] the parsed proxy URI
70
- # @example Get the proxy URI
71
- # connection.proxy_uri
72
- attr_reader :proxy_uri
73
-
74
- def_delegator :proxy_uri, :host, :proxy_host
75
- def_delegator :proxy_uri, :port, :proxy_port
76
- def_delegator :proxy_uri, :user, :proxy_user
77
- def_delegator :proxy_uri, :password, :proxy_pass
78
-
79
- # Initialize a new connection
80
- #
81
- # @api public
82
- # @param open_timeout [Integer] the timeout for opening connections in seconds
83
- # @param read_timeout [Integer] the timeout for reading responses in seconds
84
- # @param write_timeout [Integer] the timeout for writing requests in seconds
85
- # @param debug_output [IO] the IO object for debug output
86
- # @param proxy_url [String, nil] the proxy URL for requests
87
- # @return [Connection] a new connection instance
88
- # @example Create a connection with default settings
89
- # connection = X::Connection.new
90
- # @example Create a connection with custom timeouts
91
- # connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
92
- def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
93
- write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil)
94
- @open_timeout = open_timeout
95
- @read_timeout = read_timeout
96
- @write_timeout = write_timeout
97
- @debug_output = debug_output
98
- self.proxy_url = proxy_url unless proxy_url.nil?
99
- end
100
-
101
- # Perform an HTTP request
102
- #
103
- # @api public
104
- # @param request [Net::HTTPRequest] the HTTP request to perform
105
- # @return [Net::HTTPResponse] the HTTP response
106
- # @raise [NetworkError] if a network error occurs
107
- # @example Perform a request
108
- # response = connection.perform(request: request)
109
- def perform(request:)
110
- host = request.uri.host || DEFAULT_HOST
111
- port = request.uri.port || DEFAULT_PORT
112
- http_client = build_http_client(host, port)
113
- http_client.use_ssl = request.uri.scheme.eql?("https")
114
- http_client.request(request)
115
- rescue *NETWORK_ERRORS => e
116
- raise NetworkError, "Network error: #{e}"
117
- end
118
-
119
- # Perform a streaming HTTP request
120
- #
121
- # @api public
122
- # @param request [Net::HTTPRequest] the HTTP request to perform
123
- # @yield [Net::HTTPResponse] the HTTP response for streaming
124
- # @return [void]
125
- # @raise [NetworkError] if a network error occurs
126
- # @example Perform a streaming request
127
- # connection.perform_stream(request: request) { |response| response.read_body { |chunk| } }
128
- def perform_stream(request:, &)
129
- host = request.uri.host || DEFAULT_HOST
130
- port = request.uri.port || DEFAULT_PORT
131
- http_client = build_http_client(host, port)
132
- http_client.use_ssl = request.uri.scheme.eql?("https")
133
- http_client.request(request, &)
134
- rescue *NETWORK_ERRORS => e
135
- raise NetworkError, "Network error: #{e}"
136
- end
137
-
138
- # Set the proxy URL for requests
139
- #
140
- # @api public
141
- # @param proxy_url [String] the proxy URL
142
- # @return [void]
143
- # @raise [ArgumentError] if the proxy URL is invalid
144
- # @example Set the proxy URL
145
- # connection.proxy_url = "http://proxy.example.com:8080"
146
- def proxy_url=(proxy_url)
147
- @proxy_url = proxy_url
148
- proxy_uri = URI(proxy_url)
149
- raise ArgumentError, "Invalid proxy URL: #{proxy_uri}" unless proxy_uri.is_a?(URI::HTTP)
150
-
151
- @proxy_uri = proxy_uri
152
- end
153
-
154
- private
155
-
156
- # Build an HTTP client for the given host and port
157
- # @api private
158
- # @param host [String] the host to connect to
159
- # @param port [Integer] the port to connect to
160
- # @return [Net::HTTP] the HTTP client
161
- def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
162
- http_client = if proxy_uri
163
- Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
164
- else
165
- Net::HTTP.new(host, port)
166
- end
167
- configure_http_client(http_client)
168
- end
169
-
170
- # Configure an HTTP client with timeout settings
171
- # @api private
172
- # @param http_client [Net::HTTP] the HTTP client to configure
173
- # @return [Net::HTTP] the configured HTTP client
174
- def configure_http_client(http_client)
175
- http_client.tap do |c|
176
- c.open_timeout = open_timeout
177
- c.read_timeout = read_timeout
178
- c.write_timeout = write_timeout
179
- c.set_debug_output(debug_output)
180
- end
181
- end
182
- end
183
- end
@@ -1,6 +0,0 @@
1
- require_relative "server_error"
2
-
3
- module X
4
- # Error raised for HTTP 502 Bad Gateway responses
5
- class BadGateway < ServerError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 400 Bad Request responses
5
- class BadRequest < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "http_error"
2
-
3
- module X
4
- # Base class for client errors (4xx HTTP status codes)
5
- class ClientError < HTTPError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 409 Conflict responses
5
- class ConnectionException < ClientError; end
6
- end
@@ -1,4 +0,0 @@
1
- module X
2
- # Base error class for all X API errors
3
- class Error < StandardError; end
4
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 403 Forbidden responses
5
- class Forbidden < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "server_error"
2
-
3
- module X
4
- # Error raised for HTTP 504 Gateway Timeout responses
5
- class GatewayTimeout < ServerError; end
6
- end
data/lib/x/errors/gone.rb DELETED
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 410 Gone responses
5
- class Gone < ClientError; end
6
- end
@@ -1,84 +0,0 @@
1
- require "json"
2
- require_relative "error"
3
-
4
- module X
5
- # Base class for HTTP errors from the X API
6
- # @api public
7
- class HTTPError < Error
8
- # Regular expression to match JSON content types
9
- JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
10
-
11
- # The HTTP response
12
- # @api public
13
- # @return [Net::HTTPResponse] the HTTP response
14
- # @example Get the response
15
- # error.response
16
- attr_reader :response
17
-
18
- # The HTTP status code
19
- # @api public
20
- # @return [String] the HTTP status code
21
- # @example Get the status code
22
- # error.code
23
- attr_reader :code
24
-
25
- # Initialize a new HTTPError
26
- #
27
- # @api public
28
- # @param response [Net::HTTPResponse] the HTTP response
29
- # @return [HTTPError] a new instance
30
- # @example Create an HTTP error
31
- # error = X::HTTPError.new(response: response)
32
- def initialize(response:)
33
- super(error_message(response))
34
- @response = response
35
- @code = response.code
36
- end
37
-
38
- # Get the error message from the response
39
- #
40
- # @api public
41
- # @param response [Net::HTTPResponse] the HTTP response
42
- # @return [String] the error message
43
- # @example Get the error message
44
- # error.error_message(response)
45
- def error_message(response)
46
- if json?(response)
47
- message_from_json_response(response)
48
- else
49
- response.message
50
- end
51
- end
52
-
53
- # Extract error message from a JSON response
54
- #
55
- # @api public
56
- # @param response [Net::HTTPResponse] the HTTP response
57
- # @return [String] the error message
58
- # @example Get error message from JSON
59
- # error.message_from_json_response(response)
60
- def message_from_json_response(response)
61
- response_object = JSON.parse(response.body)
62
- if response_object["errors"].instance_of?(Array)
63
- response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
64
- elsif response_object.key?("title") && response_object.key?("detail")
65
- "#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
66
- elsif response_object.key?("error")
67
- response_object.fetch("error")
68
- else
69
- response.message
70
- end
71
- end
72
-
73
- # Check if the response contains JSON
74
- #
75
- # @api public
76
- # @param response [Net::HTTPResponse] the HTTP response
77
- # @return [Boolean] true if the response is JSON
78
- # @example Check if response is JSON
79
- # error.json?(response)
80
- def json?(response)
81
- JSON_CONTENT_TYPE_REGEXP === response["content-type"]
82
- end
83
- end
84
- end
@@ -1,6 +0,0 @@
1
- require_relative "server_error"
2
-
3
- module X
4
- # Error raised for HTTP 500 Internal Server Error responses
5
- class InternalServerError < ServerError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "error"
2
-
3
- module X
4
- # Error raised when a file's MIME type cannot be determined or is unsupported
5
- class InvalidMediaType < Error; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "error"
2
-
3
- module X
4
- # Error raised when a network error occurs
5
- class NetworkError < Error; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 406 Not Acceptable responses
5
- class NotAcceptable < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 404 Not Found responses
5
- class NotFound < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 413 Payload Too Large responses
5
- class PayloadTooLarge < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "http_error"
2
-
3
- module X
4
- # Base class for server errors (5xx HTTP status codes)
5
- class ServerError < HTTPError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "server_error"
2
-
3
- module X
4
- # Error raised for HTTP 503 Service Unavailable responses
5
- class ServiceUnavailable < ServerError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "error"
2
-
3
- module X
4
- # Error raised when too many redirects are encountered
5
- class TooManyRedirects < Error; end
6
- end
@@ -1,58 +0,0 @@
1
- require_relative "client_error"
2
- require_relative "../rate_limit"
3
-
4
- module X
5
- # Error raised when rate limit is exceeded (HTTP 429)
6
- # @api public
7
- class TooManyRequests < ClientError
8
- # Get the most restrictive rate limit
9
- #
10
- # @api public
11
- # @return [RateLimit, nil] the rate limit with the latest reset time
12
- # @example Get the rate limit
13
- # error.rate_limit
14
- def rate_limit
15
- rate_limits.max_by(&:reset_at)
16
- end
17
-
18
- # Get all rate limits from the response
19
- #
20
- # @api public
21
- # @return [Array<RateLimit>] the rate limits that are exhausted
22
- # @example Get all rate limits
23
- # error.rate_limits
24
- def rate_limits
25
- @rate_limits ||= RateLimit::TYPES.filter_map do |type|
26
- RateLimit.new(type:, response:) if response["x-#{type}-remaining"].eql?("0")
27
- end
28
- end
29
-
30
- # Get the time when the rate limit resets
31
- #
32
- # @api public
33
- # @return [Time] the reset time
34
- # @example Get the reset time
35
- # error.reset_at
36
- def reset_at
37
- rate_limit&.reset_at || Time.at(0)
38
- end
39
-
40
- # Get the seconds until the rate limit resets
41
- #
42
- # @api public
43
- # @return [Integer] the seconds until reset
44
- # @example Get the time until reset
45
- # error.reset_in
46
- def reset_in
47
- [(reset_at - Time.now).ceil, 0].max
48
- end
49
-
50
- # @!method retry_after
51
- # Alias for reset_in, returns the seconds to wait before retrying
52
- # @api public
53
- # @return [Integer] the seconds to wait before retrying
54
- # @example Get the retry delay
55
- # error.retry_after
56
- alias_method :retry_after, :reset_in
57
- end
58
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 401 Unauthorized responses
5
- class Unauthorized < ClientError; end
6
- end
@@ -1,6 +0,0 @@
1
- require_relative "client_error"
2
-
3
- module X
4
- # Error raised for HTTP 422 Unprocessable Entity responses
5
- class UnprocessableEntity < ClientError; end
6
- end
@@ -1,36 +0,0 @@
1
- module X
2
- # Validates media upload parameters
3
- # @api public
4
- module MediaUploadValidator
5
- module_function
6
-
7
- # Valid media category values
8
- MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
9
-
10
- # Validate that a file path exists
11
- #
12
- # @api private
13
- # @param file_path [String] the file path to validate
14
- # @return [void]
15
- # @raise [RuntimeError] if the file does not exist
16
- # @example Validate a file path
17
- # MediaUploadValidator.validate_file_path!(file_path: "image.png")
18
- def validate_file_path!(file_path:)
19
- raise "File not found: #{file_path}" unless File.exist?(file_path)
20
- end
21
-
22
- # Validate that a media category is valid
23
- #
24
- # @api private
25
- # @param media_category [String] the media category to validate
26
- # @return [void]
27
- # @raise [ArgumentError] if the media category is invalid
28
- # @example Validate a media category
29
- # MediaUploadValidator.validate_media_category!(media_category: "tweet_image")
30
- def validate_media_category!(media_category:)
31
- return if MEDIA_CATEGORIES.include?(media_category.downcase)
32
-
33
- raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
34
- end
35
- end
36
- end