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
@@ -1,234 +0,0 @@
1
- require "json"
2
- require "securerandom"
3
- require "tmpdir"
4
- require_relative "errors/invalid_media_type"
5
- require_relative "media_upload_validator"
6
-
7
- module X
8
- # Uploads media files to the X API
9
- # @api public
10
- module MediaUploader
11
- extend self
12
-
13
- # Maximum number of retry attempts for failed uploads
14
- MAX_RETRIES = 3
15
- # Number of bytes per megabyte
16
- BYTES_PER_MB = 1_048_576
17
- # Media category constants
18
- DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MediaUploadValidator::MEDIA_CATEGORIES
19
- # Supported MIME types
20
- MIME_TYPES = %w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
21
- # MIME type constants
22
- GIF_MIME_TYPE, JPEG_MIME_TYPE, MP4_MIME_TYPE, PNG_MIME_TYPE, SUBRIP_MIME_TYPE, WEBP_MIME_TYPE = MIME_TYPES
23
- # Mapping of file extensions to MIME types
24
- MIME_TYPE_MAP = {"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
25
- "png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
26
- # Processing states that indicate completion
27
- PROCESSING_INFO_STATES = %w[failed succeeded].freeze
28
-
29
- # Upload a file to the X API
30
- #
31
- # @api public
32
- # @param client [Client] the X API client
33
- # @param file_path [String] the path to the file to upload
34
- # @param media_category [String] the media category
35
- # @param boundary [String] the multipart boundary
36
- # @return [Hash, nil] the upload response data
37
- # @raise [RuntimeError] if the file does not exist
38
- # @example Upload an image
39
- # MediaUploader.upload(client: client, file_path: "image.png", media_category: "tweet_image")
40
- def upload(client:, file_path:, media_category:, boundary: SecureRandom.hex)
41
- MediaUploadValidator.validate_file_path!(file_path:)
42
- upload_binary(client:, content: File.binread(file_path), media_category:, boundary:)
43
- end
44
-
45
- # Upload binary content to the X API
46
- #
47
- # @api public
48
- # @param client [Client] the X API client
49
- # @param content [String] the binary content to upload
50
- # @param media_category [String] the media category
51
- # @param boundary [String] the multipart boundary
52
- # @return [Hash, nil] the upload response data
53
- # @raise [ArgumentError] if the media category is invalid
54
- # @example Upload binary content
55
- # MediaUploader.upload_binary(client: client, content: data, media_category: "tweet_image")
56
- def upload_binary(client:, content:, media_category:, boundary: SecureRandom.hex)
57
- MediaUploadValidator.validate_media_category!(media_category:)
58
- upload_body = construct_upload_body(content:, media_category:, boundary:)
59
- headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
60
- client.post("media/upload", upload_body, headers:)&.fetch("data")
61
- end
62
-
63
- # Perform a chunked upload for large files
64
- #
65
- # @api public
66
- # @param client [Client] the X API client
67
- # @param file_path [String] the path to the file to upload
68
- # @param media_category [String] the media category
69
- # @param media_type [String] the MIME type of the media
70
- # @param boundary [String] the multipart boundary
71
- # @param chunk_size_mb [Integer] the size of each chunk in megabytes
72
- # @return [Hash, nil] the upload response data
73
- # @raise [RuntimeError] if the file does not exist
74
- # @raise [ArgumentError] if the media category is invalid
75
- # @example Upload a large video
76
- # MediaUploader.chunked_upload(client: client, file_path: "video.mp4", media_category: "tweet_video")
77
- def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
78
- boundary: SecureRandom.hex, chunk_size_mb: 1)
79
- MediaUploadValidator.validate_file_path!(file_path:)
80
- MediaUploadValidator.validate_media_category!(media_category:)
81
- media = init(client:, file_path:, media_type:, media_category:)
82
- chunk_size = chunk_size_mb * BYTES_PER_MB
83
- append(client:, file_paths: split(file_path, chunk_size), media:, boundary:)
84
- client.post("media/upload/#{media["id"]}/finalize")&.fetch("data")
85
- end
86
-
87
- # Wait for media processing to complete
88
- #
89
- # @api public
90
- # @param client [Client] the X API client
91
- # @param media [Hash] the media object with an id
92
- # @return [Hash, nil] the processing status
93
- # @example Wait for processing
94
- # MediaUploader.await_processing(client: client, media: media)
95
- def await_processing(client:, media:)
96
- loop do
97
- status = client.get("media/upload?command=STATUS&media_id=#{media["id"]}")&.fetch("data")
98
- return status if status.nil? || !status["processing_info"] || PROCESSING_INFO_STATES.include?(status["processing_info"]["state"])
99
-
100
- sleep status["processing_info"]["check_after_secs"].to_i
101
- end
102
- end
103
-
104
- # Wait for media processing and raise on failure
105
- #
106
- # @api public
107
- # @param client [Client] the X API client
108
- # @param media [Hash] the media object with an id
109
- # @return [Hash, nil] the processing status
110
- # @raise [RuntimeError] if media processing failed
111
- # @example Wait for processing with error handling
112
- # MediaUploader.await_processing!(client: client, media: media)
113
- def await_processing!(client:, media:)
114
- status = await_processing(client:, media:)
115
- raise "Media processing failed" if status&.dig("processing_info", "state") == "failed"
116
-
117
- status
118
- end
119
-
120
- # Infer the media type from file path and category
121
- #
122
- # @api public
123
- # @param file_path [String] the file path
124
- # @param media_category [String] the media category
125
- # @return [String] the inferred MIME type
126
- # @raise [InvalidMediaType] if the MIME type cannot be determined
127
- # @example MediaUploader.infer_media_type("image.png", "tweet_image") #=> "image/png"
128
- def infer_media_type(file_path, media_category)
129
- case media_category.downcase
130
- when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
131
- when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
132
- when SUBTITLES then SUBRIP_MIME_TYPE
133
- else
134
- extension = File.extname(file_path).delete(".").downcase
135
- MIME_TYPE_MAP.fetch(extension) do
136
- raise InvalidMediaType, "unable to determine MIME type from file extension: #{file_path.inspect}"
137
- end
138
- end
139
- end
140
-
141
- private
142
-
143
- # Split a file into chunks
144
- # @api private
145
- # @param file_path [String] the file path
146
- # @param chunk_size [Integer] the chunk size in bytes
147
- # @return [Array<String>] the paths to the chunk files
148
- def split(file_path, chunk_size)
149
- file_size = File.size(file_path)
150
- segment_count = (file_size.to_f / chunk_size).ceil
151
- (0...segment_count).map do |segment_index|
152
- segment_path = "#{Dir.mktmpdir}/x#{format("%03d", segment_index + 1)}"
153
- File.binwrite(segment_path, File.binread(file_path, chunk_size, segment_index * chunk_size))
154
- segment_path
155
- end
156
- end
157
-
158
- # Initialize a chunked upload
159
- # @api private
160
- # @param client [Client] the X API client
161
- # @param file_path [String] the file path
162
- # @param media_type [String] the MIME type
163
- # @param media_category [String] the media category
164
- # @return [Hash, nil] the initialization response
165
- def init(client:, file_path:, media_type:, media_category:)
166
- total_bytes = File.size(file_path)
167
- data = {media_type:, media_category:, total_bytes:}.to_json
168
- client.post("media/upload/initialize", data)&.fetch("data")
169
- end
170
-
171
- # Append chunks to a chunked upload
172
- # @api private
173
- # @param client [Client] the X API client
174
- # @param file_paths [Array<String>] the chunk file paths
175
- # @param media [Hash] the media object
176
- # @param boundary [String] the multipart boundary
177
- # @return [void]
178
- def append(client:, file_paths:, media:, boundary: SecureRandom.hex)
179
- threads = file_paths.map.with_index do |file_path, index|
180
- Thread.new do
181
- upload_body = construct_upload_body(content: File.binread(file_path), segment_index: index, boundary:)
182
- headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
183
- upload_chunk(client:, media_id: media["id"], upload_body:, file_path:, headers:)
184
- end
185
- end
186
- threads.each(&:join)
187
- end
188
-
189
- # Upload a single chunk with retry logic
190
- # @api private
191
- # @param client [Client] the X API client
192
- # @param media_id [String] the media ID
193
- # @param upload_body [String] the upload body
194
- # @param file_path [String] the chunk file path
195
- # @param headers [Hash] the request headers
196
- # @return [void]
197
- def upload_chunk(client:, media_id:, upload_body:, file_path:, headers: {})
198
- client.post("media/upload/#{media_id}/append", upload_body, headers:)
199
- rescue NetworkError, ServerError
200
- retries ||= 0
201
- ((retries += 1) < MAX_RETRIES) ? retry : raise
202
- ensure
203
- cleanup_file(file_path)
204
- end
205
-
206
- # Clean up a temporary file
207
- # @api private
208
- # @param file_path [String] the file path
209
- # @return [void]
210
- def cleanup_file(file_path)
211
- dirname = File.dirname(file_path)
212
- File.delete(file_path)
213
- Dir.delete(dirname) if Dir.empty?(dirname)
214
- end
215
-
216
- # Construct the multipart upload body
217
- # @api private
218
- # @param content [String] the content to upload
219
- # @param media_category [String, nil] the media category
220
- # @param segment_index [Integer, nil] the segment index
221
- # @param boundary [String] the multipart boundary
222
- # @return [String] the upload body
223
- def construct_upload_body(content:, media_category: nil, segment_index: nil, boundary: SecureRandom.hex)
224
- body = ""
225
- body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"segment_index\"\r\n\r\n#{segment_index}\r\n" if segment_index
226
- body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"media_category\"\r\n\r\n#{media_category}\r\n" if media_category
227
- "#{body}--#{boundary}\r\n" \
228
- "Content-Disposition: form-data; name=\"media\"\r\n" \
229
- "Content-Type: application/octet-stream\r\n\r\n" \
230
- "#{content}\r\n" \
231
- "--#{boundary}--\r\n"
232
- end
233
- end
234
- end
@@ -1,169 +0,0 @@
1
- require "base64"
2
- require "json"
3
- require "net/http"
4
- require "uri"
5
- require_relative "authenticator"
6
- require_relative "connection"
7
-
8
- module X
9
- # Handles OAuth 2.0 authentication with token refresh capability
10
- # @api public
11
- class OAuth2Authenticator < Authenticator
12
- # Path for the OAuth 2.0 token endpoint
13
- TOKEN_PATH = "/2/oauth2/token".freeze
14
- # Host for token refresh requests
15
- TOKEN_HOST = "api.x.com".freeze
16
- # Grant type for token refresh
17
- REFRESH_GRANT_TYPE = "refresh_token".freeze
18
- # Buffer time in seconds to account for clock skew and network latency
19
- EXPIRATION_BUFFER = 30
20
-
21
- # The OAuth 2.0 client ID
22
- # @api public
23
- # @return [String] the client ID
24
- # @example Get the client ID
25
- # authenticator.client_id
26
- attr_accessor :client_id
27
- # The OAuth 2.0 client secret
28
- # @api public
29
- # @return [String] the client secret
30
- # @example Get the client secret
31
- # authenticator.client_secret
32
- attr_accessor :client_secret
33
- # The OAuth 2.0 access token
34
- # @api public
35
- # @return [String] the access token
36
- # @example Get the access token
37
- # authenticator.access_token
38
- attr_accessor :access_token
39
- # The OAuth 2.0 refresh token
40
- # @api public
41
- # @return [String] the refresh token
42
- # @example Get the refresh token
43
- # authenticator.refresh_token
44
- attr_accessor :refresh_token
45
- # The expiration time of the access token
46
- # @api public
47
- # @return [Time, nil] the expiration time
48
- # @example Get the expiration time
49
- # authenticator.expires_at
50
- attr_accessor :expires_at
51
-
52
- # The connection for making token requests
53
- # @api public
54
- # @return [Connection] the connection instance
55
- # @example Get the connection
56
- # authenticator.connection
57
- attr_accessor :connection
58
-
59
- # Initialize a new OAuth 2.0 authenticator
60
- #
61
- # @api public
62
- # @param client_id [String] the OAuth 2.0 client ID
63
- # @param client_secret [String] the OAuth 2.0 client secret
64
- # @param access_token [String] the OAuth 2.0 access token
65
- # @param refresh_token [String] the OAuth 2.0 refresh token
66
- # @param expires_at [Time, nil] the expiration time of the access token
67
- # @param connection [Connection] the connection for making token requests
68
- # @return [OAuth2Authenticator] a new authenticator instance
69
- # @example Create an authenticator
70
- # authenticator = X::OAuth2Authenticator.new(
71
- # client_id: "id",
72
- # client_secret: "secret",
73
- # access_token: "token",
74
- # refresh_token: "refresh"
75
- # )
76
- def initialize(client_id:, client_secret:, access_token:, refresh_token:, expires_at: nil,
77
- connection: Connection.new)
78
- @client_id = client_id
79
- @client_secret = client_secret
80
- @access_token = access_token
81
- @refresh_token = refresh_token
82
- @expires_at = expires_at
83
- @connection = connection
84
- end
85
-
86
- # Generate the authentication header
87
- #
88
- # @api public
89
- # @param _request [Net::HTTPRequest, nil] the HTTP request (unused)
90
- # @return [Hash{String => String}] the authentication header
91
- # @example Get the header
92
- # authenticator.header(request)
93
- def header(_request)
94
- {AUTHENTICATION_HEADER => "Bearer #{access_token}"}
95
- end
96
-
97
- # Check if the access token has expired or will expire soon
98
- #
99
- # @api public
100
- # @return [Boolean] true if the token has expired or will expire within the buffer period
101
- # @example Check expiration
102
- # authenticator.token_expired?
103
- def token_expired?
104
- return false if expires_at.nil?
105
-
106
- Time.now >= expires_at - EXPIRATION_BUFFER
107
- end
108
-
109
- # Refresh the access token using the refresh token
110
- #
111
- # @api public
112
- # @return [Hash{String => Object}] the token response
113
- # @raise [Error] if token refresh fails
114
- # @example Refresh the token
115
- # authenticator.refresh_token!
116
- def refresh_token!
117
- response = send_token_request
118
- handle_token_response(response)
119
- end
120
-
121
- private
122
-
123
- # Send the token refresh request
124
- # @api private
125
- # @return [Net::HTTPResponse] the HTTP response
126
- def send_token_request
127
- request = build_token_request
128
- connection.perform(request: request)
129
- end
130
-
131
- # Build the token refresh request
132
- # @api private
133
- # @return [Net::HTTP::Post] the POST request
134
- def build_token_request
135
- uri = URI::HTTPS.build(host: TOKEN_HOST, path: TOKEN_PATH)
136
- request = Net::HTTP::Post.new(uri)
137
- request["Content-Type"] = "application/x-www-form-urlencoded"
138
- request["Authorization"] = "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"
139
- request.body = URI.encode_www_form(grant_type: REFRESH_GRANT_TYPE, refresh_token: refresh_token)
140
- request
141
- end
142
-
143
- # Handle the token response
144
- # @api private
145
- # @param response [Net::HTTPResponse] the HTTP response
146
- # @return [Hash{String => Object}] the parsed response body
147
- # @raise [Error] if the response indicates an error
148
- def handle_token_response(response)
149
- body = JSON.parse(response.body)
150
- rescue JSON::ParserError
151
- raise Error, "Token refresh failed"
152
- else
153
- raise Error, body["error_description"] || body["error"] || "Token refresh failed" unless response.is_a?(Net::HTTPSuccess)
154
-
155
- update_tokens(body)
156
- body
157
- end
158
-
159
- # Update tokens from the response
160
- # @api private
161
- # @param token_response [Hash{String => Object}] the token response
162
- # @return [void]
163
- def update_tokens(token_response)
164
- @access_token = token_response.fetch("access_token")
165
- @refresh_token = token_response.fetch("refresh_token") if token_response.key?("refresh_token")
166
- @expires_at = Time.now + token_response.fetch("expires_in") if token_response.key?("expires_in")
167
- end
168
- end
169
- end
@@ -1,182 +0,0 @@
1
- require "base64"
2
- require "cgi/escape"
3
- require "json"
4
- require "openssl"
5
- require "securerandom"
6
- require "uri"
7
- require_relative "authenticator"
8
-
9
- module X
10
- # Authenticator for OAuth 1.0a authentication
11
- # @api public
12
- class OAuthAuthenticator < Authenticator
13
- # OAuth version
14
- OAUTH_VERSION = "1.0".freeze
15
- # OAuth signature method
16
- OAUTH_SIGNATURE_METHOD = "HMAC-SHA1".freeze
17
- # OAuth signature algorithm
18
- OAUTH_SIGNATURE_ALGORITHM = "sha1".freeze
19
-
20
- # The API key (consumer key)
21
- # @api public
22
- # @return [String] the API key (consumer key)
23
- # @example Get or set the API key
24
- # authenticator.api_key = "key"
25
- attr_accessor :api_key
26
-
27
- # The API key secret (consumer secret)
28
- # @api public
29
- # @return [String] the API key secret (consumer secret)
30
- # @example Get or set the API key secret
31
- # authenticator.api_key_secret = "secret"
32
- attr_accessor :api_key_secret
33
-
34
- # The access token
35
- # @api public
36
- # @return [String] the access token
37
- # @example Get or set the access token
38
- # authenticator.access_token = "token"
39
- attr_accessor :access_token
40
-
41
- # The access token secret
42
- # @api public
43
- # @return [String] the access token secret
44
- # @example Get or set the access token secret
45
- # authenticator.access_token_secret = "token_secret"
46
- attr_accessor :access_token_secret
47
-
48
- # Initialize a new OAuthAuthenticator
49
- #
50
- # @api public
51
- # @param api_key [String] the API key (consumer key)
52
- # @param api_key_secret [String] the API key secret (consumer secret)
53
- # @param access_token [String] the access token
54
- # @param access_token_secret [String] the access token secret
55
- # @return [OAuthAuthenticator] a new instance
56
- # @example Create an OAuth authenticator
57
- # authenticator = X::OAuthAuthenticator.new(
58
- # api_key: "key",
59
- # api_key_secret: "secret",
60
- # access_token: "token",
61
- # access_token_secret: "token_secret"
62
- # )
63
- def initialize(api_key:, api_key_secret:, access_token:, access_token_secret:)
64
- @api_key = api_key
65
- @api_key_secret = api_key_secret
66
- @access_token = access_token
67
- @access_token_secret = access_token_secret
68
- end
69
-
70
- # Generate the OAuth authentication header for a request
71
- #
72
- # @api public
73
- # @param request [Net::HTTPRequest] the HTTP request
74
- # @return [Hash{String => String}] the authentication header with OAuth signature
75
- # @example Generate an OAuth authentication header
76
- # authenticator.header(request)
77
- def header(request)
78
- method, url, query_params = parse_request(request)
79
- {AUTHENTICATION_HEADER => build_oauth_header(method, url, query_params)}
80
- end
81
-
82
- private
83
-
84
- # Parse the request to extract method, URL, and query parameters
85
- # @api private
86
- # @param request [Net::HTTPRequest] the HTTP request
87
- # @return [Array<String, String, Hash>] the method, URL, and query parameters
88
- def parse_request(request)
89
- uri = request.uri
90
- query_params = parse_query_params(uri.query.to_s)
91
- [request.method, uri_without_query(uri), query_params]
92
- end
93
-
94
- # Parse query parameters from a query string
95
- # @api private
96
- # @param query_string [String] the query string
97
- # @return [Hash] the parsed query parameters
98
- def parse_query_params(query_string)
99
- URI.decode_www_form(query_string).to_h
100
- end
101
-
102
- # Get the URI without query parameters
103
- # @api private
104
- # @param uri [URI] the URI
105
- # @return [String] the URI without query parameters
106
- def uri_without_query(uri)
107
- "#{uri.scheme}://#{uri.host}#{uri.path}"
108
- end
109
-
110
- # Build the OAuth header value
111
- # @api private
112
- # @param method [String] the HTTP method
113
- # @param url [String] the request URL
114
- # @param query_params [Hash] the query parameters
115
- # @return [String] the OAuth header value
116
- def build_oauth_header(method, url, query_params)
117
- oauth_params = default_oauth_params
118
- all_params = query_params.merge(oauth_params)
119
- oauth_params["oauth_signature"] = generate_signature(method, url, all_params)
120
- format_oauth_header(oauth_params)
121
- end
122
-
123
- # Get the default OAuth parameters
124
- # @api private
125
- # @return [Hash] the default OAuth parameters
126
- def default_oauth_params
127
- {
128
- "oauth_consumer_key" => api_key,
129
- "oauth_nonce" => SecureRandom.hex,
130
- "oauth_signature_method" => OAUTH_SIGNATURE_METHOD,
131
- "oauth_timestamp" => Integer(Time.now).to_s,
132
- "oauth_token" => access_token,
133
- "oauth_version" => OAUTH_VERSION
134
- }
135
- end
136
-
137
- # Generate the OAuth signature
138
- # @api private
139
- # @param method [String] the HTTP method
140
- # @param url [String] the request URL
141
- # @param params [Hash] the combined parameters
142
- # @return [String] the OAuth signature
143
- def generate_signature(method, url, params)
144
- base_string = signature_base_string(method, url, params)
145
- hmac_signature(base_string)
146
- end
147
-
148
- # Generate the HMAC signature
149
- # @api private
150
- # @param base_string [String] the signature base string
151
- # @return [String] the Base64-encoded HMAC signature
152
- def hmac_signature(base_string)
153
- hmac = OpenSSL::HMAC.digest(OAUTH_SIGNATURE_ALGORITHM, signing_key, base_string)
154
- Base64.strict_encode64(hmac)
155
- end
156
-
157
- # Build the signature base string
158
- # @api private
159
- # @param method [String] the HTTP method
160
- # @param url [String] the request URL
161
- # @param params [Hash] the combined parameters
162
- # @return [String] the signature base string
163
- def signature_base_string(method, url, params)
164
- "#{method}&#{CGI.escapeURIComponent(url)}&#{CGI.escapeURIComponent(URI.encode_www_form(params.sort).gsub("+", "%20"))}"
165
- end
166
-
167
- # Get the signing key
168
- # @api private
169
- # @return [String] the signing key
170
- def signing_key
171
- "#{api_key_secret}&#{access_token_secret}"
172
- end
173
-
174
- # Format the OAuth header value
175
- # @api private
176
- # @param params [Hash] the OAuth parameters
177
- # @return [String] the formatted OAuth header value
178
- def format_oauth_header(params)
179
- "OAuth #{params.sort.map { |k, v| "#{k}=\"#{CGI.escapeURIComponent(v)}\"" }.join(", ")}"
180
- end
181
- end
182
- end
data/lib/x/rate_limit.rb DELETED
@@ -1,89 +0,0 @@
1
- module X
2
- # Represents rate limit information from an API response
3
- # @api public
4
- class RateLimit
5
- # Rate limit type identifier
6
- RATE_LIMIT_TYPE = "rate-limit".freeze
7
- # App limit type identifier
8
- APP_LIMIT_TYPE = "app-limit-24hour".freeze
9
- # User limit type identifier
10
- USER_LIMIT_TYPE = "user-limit-24hour".freeze
11
- # All supported rate limit types
12
- TYPES = [RATE_LIMIT_TYPE, APP_LIMIT_TYPE, USER_LIMIT_TYPE].freeze
13
-
14
- # The type of rate limit
15
- # @api public
16
- # @return [String] the type of rate limit
17
- # @example Get or set the rate limit type
18
- # rate_limit.type = "rate-limit"
19
- attr_accessor :type
20
-
21
- # The HTTP response containing rate limit headers
22
- # @api public
23
- # @return [Net::HTTPResponse] the HTTP response containing rate limit headers
24
- # @example Get or set the response
25
- # rate_limit.response = http_response
26
- attr_accessor :response
27
-
28
- # Initialize a new RateLimit
29
- #
30
- # @api public
31
- # @param type [String] the type of rate limit
32
- # @param response [Net::HTTPResponse] the HTTP response containing rate limit headers
33
- # @return [RateLimit] a new instance
34
- # @example Create a rate limit instance
35
- # rate_limit = X::RateLimit.new(type: "rate-limit", response: response)
36
- def initialize(type:, response:)
37
- @type = type
38
- @response = response
39
- end
40
-
41
- # Get the rate limit maximum
42
- #
43
- # @api public
44
- # @return [Integer] the maximum number of requests allowed
45
- # @example Get the rate limit
46
- # rate_limit.limit
47
- def limit
48
- Integer(response.fetch("x-#{type}-limit"))
49
- end
50
-
51
- # Get the remaining requests
52
- #
53
- # @api public
54
- # @return [Integer] the number of requests remaining
55
- # @example Get the remaining requests
56
- # rate_limit.remaining
57
- def remaining
58
- Integer(response.fetch("x-#{type}-remaining"))
59
- end
60
-
61
- # Get the time when the rate limit resets
62
- #
63
- # @api public
64
- # @return [Time] the time when the rate limit resets
65
- # @example Get the reset time
66
- # rate_limit.reset_at
67
- def reset_at
68
- Time.at(Integer(response.fetch("x-#{type}-reset")))
69
- end
70
-
71
- # Get the seconds until the rate limit resets
72
- #
73
- # @api public
74
- # @return [Integer] the seconds until the rate limit resets
75
- # @example Get the reset time in seconds
76
- # rate_limit.reset_in
77
- def reset_in
78
- [(reset_at - Time.now).ceil, 0].max
79
- end
80
-
81
- # @!method retry_after
82
- # Alias for reset_in, returns the seconds until the rate limit resets
83
- # @api public
84
- # @return [Integer] the seconds until the rate limit resets
85
- # @example Get the retry after time
86
- # rate_limit.retry_after
87
- alias_method :retry_after, :reset_in
88
- end
89
- end