x 0.18.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +651 -15
  4. data/README.md +513 -67
  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 -346
  11. metadata +57 -48
  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 -160
  18. data/lib/x/client_credentials.rb +0 -208
  19. data/lib/x/connection.rb +0 -166
  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
data/lib/x/connection.rb DELETED
@@ -1,166 +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
- # Default debug output destination
24
- DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
25
- # Network errors that should be wrapped in NetworkError
26
- NETWORK_ERRORS = [
27
- Errno::ECONNREFUSED,
28
- Errno::ECONNRESET,
29
- Net::OpenTimeout,
30
- Net::ReadTimeout,
31
- OpenSSL::SSL::SSLError
32
- ].freeze
33
-
34
- # The timeout for opening connections in seconds
35
- # @api public
36
- # @return [Integer] the timeout for opening connections in seconds
37
- # @example Get or set the open timeout
38
- # connection.open_timeout = 30
39
- attr_accessor :open_timeout
40
-
41
- # The timeout for reading responses in seconds
42
- # @api public
43
- # @return [Integer] the timeout for reading responses in seconds
44
- # @example Get or set the read timeout
45
- # connection.read_timeout = 30
46
- attr_accessor :read_timeout
47
-
48
- # The timeout for writing requests in seconds
49
- # @api public
50
- # @return [Integer] the timeout for writing requests in seconds
51
- # @example Get or set the write timeout
52
- # connection.write_timeout = 30
53
- attr_accessor :write_timeout
54
-
55
- # The IO object for debug output
56
- # @api public
57
- # @return [IO] the IO object for debug output
58
- # @example Get or set the debug output
59
- # connection.debug_output = $stderr
60
- attr_accessor :debug_output
61
-
62
- # The proxy URL for requests
63
- # @api public
64
- # @return [String, nil] the proxy URL for requests
65
- # @example Get the proxy URL
66
- # connection.proxy_url
67
- attr_reader :proxy_url
68
-
69
- # The parsed proxy URI
70
- # @api public
71
- # @return [URI, nil] the parsed proxy URI
72
- # @example Get the proxy URI
73
- # connection.proxy_uri
74
- attr_reader :proxy_uri
75
-
76
- def_delegator :proxy_uri, :host, :proxy_host
77
- def_delegator :proxy_uri, :port, :proxy_port
78
- def_delegator :proxy_uri, :user, :proxy_user
79
- def_delegator :proxy_uri, :password, :proxy_pass
80
-
81
- # Initialize a new connection
82
- #
83
- # @api public
84
- # @param open_timeout [Integer] the timeout for opening connections in seconds
85
- # @param read_timeout [Integer] the timeout for reading responses in seconds
86
- # @param write_timeout [Integer] the timeout for writing requests in seconds
87
- # @param debug_output [IO] the IO object for debug output
88
- # @param proxy_url [String, nil] the proxy URL for requests
89
- # @return [Connection] a new connection instance
90
- # @example Create a connection with default settings
91
- # connection = X::Connection.new
92
- # @example Create a connection with custom timeouts
93
- # connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
94
- def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
95
- write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
96
- @open_timeout = open_timeout
97
- @read_timeout = read_timeout
98
- @write_timeout = write_timeout
99
- @debug_output = debug_output
100
- self.proxy_url = proxy_url unless proxy_url.nil?
101
- end
102
-
103
- # Perform an HTTP request
104
- #
105
- # @api public
106
- # @param request [Net::HTTPRequest] the HTTP request to perform
107
- # @return [Net::HTTPResponse] the HTTP response
108
- # @raise [NetworkError] if a network error occurs
109
- # @example Perform a request
110
- # response = connection.perform(request: request)
111
- def perform(request:)
112
- host = request.uri.host || DEFAULT_HOST
113
- port = request.uri.port || DEFAULT_PORT
114
- http_client = build_http_client(host, port)
115
- http_client.use_ssl = request.uri.scheme.eql?("https")
116
- http_client.request(request)
117
- rescue *NETWORK_ERRORS => e
118
- raise NetworkError, "Network error: #{e}"
119
- end
120
-
121
- # Set the proxy URL for requests
122
- #
123
- # @api public
124
- # @param proxy_url [String] the proxy URL
125
- # @return [void]
126
- # @raise [ArgumentError] if the proxy URL is invalid
127
- # @example Set the proxy URL
128
- # connection.proxy_url = "http://proxy.example.com:8080"
129
- def proxy_url=(proxy_url)
130
- @proxy_url = proxy_url
131
- proxy_uri = URI(proxy_url)
132
- raise ArgumentError, "Invalid proxy URL: #{proxy_uri}" unless proxy_uri.is_a?(URI::HTTP)
133
-
134
- @proxy_uri = proxy_uri
135
- end
136
-
137
- private
138
-
139
- # Build an HTTP client for the given host and port
140
- # @api private
141
- # @param host [String] the host to connect to
142
- # @param port [Integer] the port to connect to
143
- # @return [Net::HTTP] the HTTP client
144
- def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
145
- http_client = if proxy_uri
146
- Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
147
- else
148
- Net::HTTP.new(host, port)
149
- end
150
- configure_http_client(http_client)
151
- end
152
-
153
- # Configure an HTTP client with timeout settings
154
- # @api private
155
- # @param http_client [Net::HTTP] the HTTP client to configure
156
- # @return [Net::HTTP] the configured HTTP client
157
- def configure_http_client(http_client)
158
- http_client.tap do |c|
159
- c.open_timeout = open_timeout
160
- c.read_timeout = read_timeout
161
- c.write_timeout = write_timeout
162
- c.set_debug_output(debug_output)
163
- end
164
- end
165
- end
166
- 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