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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +4 -4
- data/lib/x/account_uploader.rb +168 -0
- data/lib/x/authenticator.rb +12 -0
- data/lib/x/bearer_token_authenticator.rb +22 -1
- data/lib/x/client.rb +95 -57
- data/lib/x/client_credentials.rb +208 -0
- data/lib/x/connection.rb +88 -2
- data/lib/x/errors/bad_gateway.rb +1 -0
- data/lib/x/errors/bad_request.rb +1 -0
- data/lib/x/errors/client_error.rb +1 -0
- data/lib/x/errors/connection_exception.rb +1 -0
- data/lib/x/errors/error.rb +1 -0
- data/lib/x/errors/forbidden.rb +1 -0
- data/lib/x/errors/gateway_timeout.rb +1 -0
- data/lib/x/errors/gone.rb +1 -0
- data/lib/x/errors/http_error.rb +47 -4
- data/lib/x/errors/internal_server_error.rb +1 -0
- data/lib/x/errors/invalid_media_type.rb +6 -0
- data/lib/x/errors/network_error.rb +1 -0
- data/lib/x/errors/not_acceptable.rb +1 -0
- data/lib/x/errors/not_found.rb +1 -0
- data/lib/x/errors/payload_too_large.rb +1 -0
- data/lib/x/errors/server_error.rb +1 -0
- data/lib/x/errors/service_unavailable.rb +1 -0
- data/lib/x/errors/too_many_redirects.rb +1 -0
- data/lib/x/errors/too_many_requests.rb +32 -0
- data/lib/x/errors/unauthorized.rb +1 -0
- data/lib/x/errors/unprocessable_entity.rb +1 -0
- data/lib/x/media_upload_validator.rb +19 -0
- data/lib/x/media_uploader.rb +117 -5
- data/lib/x/oauth2_authenticator.rb +169 -0
- data/lib/x/oauth_authenticator.rb +99 -2
- data/lib/x/rate_limit.rb +57 -1
- data/lib/x/redirect_handler.rb +55 -1
- data/lib/x/request_builder.rb +36 -0
- data/lib/x/response_parser.rb +21 -0
- data/lib/x/version.rb +2 -1
- data/sig/x.rbs +78 -17
- metadata +6 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
module X
|
|
2
|
+
# Mixin for client authentication credentials
|
|
3
|
+
# @api private
|
|
4
|
+
module ClientCredentials
|
|
5
|
+
# The API key for OAuth 1.0a authentication
|
|
6
|
+
# @api public
|
|
7
|
+
# @return [String, nil] the API key for OAuth 1.0a authentication
|
|
8
|
+
# @example Get the API key
|
|
9
|
+
# client.api_key
|
|
10
|
+
attr_reader :api_key
|
|
11
|
+
|
|
12
|
+
# The API key secret for OAuth 1.0a authentication
|
|
13
|
+
# @api public
|
|
14
|
+
# @return [String, nil] the API key secret for OAuth 1.0a authentication
|
|
15
|
+
# @example Get the API key secret
|
|
16
|
+
# client.api_key_secret
|
|
17
|
+
attr_reader :api_key_secret
|
|
18
|
+
|
|
19
|
+
# The access token for OAuth authentication
|
|
20
|
+
# @api public
|
|
21
|
+
# @return [String, nil] the access token for OAuth authentication
|
|
22
|
+
# @example Get the access token
|
|
23
|
+
# client.access_token
|
|
24
|
+
attr_reader :access_token
|
|
25
|
+
|
|
26
|
+
# The access token secret for OAuth 1.0a authentication
|
|
27
|
+
# @api public
|
|
28
|
+
# @return [String, nil] the access token secret for OAuth 1.0a authentication
|
|
29
|
+
# @example Get the access token secret
|
|
30
|
+
# client.access_token_secret
|
|
31
|
+
attr_reader :access_token_secret
|
|
32
|
+
|
|
33
|
+
# The bearer token for authentication
|
|
34
|
+
# @api public
|
|
35
|
+
# @return [String, nil] the bearer token for authentication
|
|
36
|
+
# @example Get the bearer token
|
|
37
|
+
# client.bearer_token
|
|
38
|
+
attr_reader :bearer_token
|
|
39
|
+
|
|
40
|
+
# The OAuth 2.0 client ID
|
|
41
|
+
# @api public
|
|
42
|
+
# @return [String, nil] the OAuth 2.0 client ID
|
|
43
|
+
# @example Get the client ID
|
|
44
|
+
# client.client_id
|
|
45
|
+
attr_reader :client_id
|
|
46
|
+
|
|
47
|
+
# The OAuth 2.0 client secret
|
|
48
|
+
# @api public
|
|
49
|
+
# @return [String, nil] the OAuth 2.0 client secret
|
|
50
|
+
# @example Get the client secret
|
|
51
|
+
# client.client_secret
|
|
52
|
+
attr_reader :client_secret
|
|
53
|
+
|
|
54
|
+
# The OAuth 2.0 refresh token
|
|
55
|
+
# @api public
|
|
56
|
+
# @return [String, nil] the OAuth 2.0 refresh token
|
|
57
|
+
# @example Get the refresh token
|
|
58
|
+
# client.refresh_token
|
|
59
|
+
attr_reader :refresh_token
|
|
60
|
+
|
|
61
|
+
# Set the API key for OAuth 1.0a authentication
|
|
62
|
+
#
|
|
63
|
+
# @api public
|
|
64
|
+
# @param api_key [String] the API key for OAuth 1.0a authentication
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @example Set the API key
|
|
67
|
+
# client.api_key = "new_key"
|
|
68
|
+
def api_key=(api_key)
|
|
69
|
+
@api_key = api_key
|
|
70
|
+
initialize_authenticator
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Set the API key secret for OAuth 1.0a authentication
|
|
74
|
+
#
|
|
75
|
+
# @api public
|
|
76
|
+
# @param api_key_secret [String] the API key secret for OAuth 1.0a authentication
|
|
77
|
+
# @return [void]
|
|
78
|
+
# @example Set the API key secret
|
|
79
|
+
# client.api_key_secret = "new_secret"
|
|
80
|
+
def api_key_secret=(api_key_secret)
|
|
81
|
+
@api_key_secret = api_key_secret
|
|
82
|
+
initialize_authenticator
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Set the access token for OAuth authentication
|
|
86
|
+
#
|
|
87
|
+
# @api public
|
|
88
|
+
# @param access_token [String] the access token for OAuth authentication
|
|
89
|
+
# @return [void]
|
|
90
|
+
# @example Set the access token
|
|
91
|
+
# client.access_token = "new_token"
|
|
92
|
+
def access_token=(access_token)
|
|
93
|
+
@access_token = access_token
|
|
94
|
+
initialize_authenticator
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Set the access token secret for OAuth 1.0a authentication
|
|
98
|
+
#
|
|
99
|
+
# @api public
|
|
100
|
+
# @param access_token_secret [String] the access token secret for OAuth 1.0a authentication
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @example Set the access token secret
|
|
103
|
+
# client.access_token_secret = "new_secret"
|
|
104
|
+
def access_token_secret=(access_token_secret)
|
|
105
|
+
@access_token_secret = access_token_secret
|
|
106
|
+
initialize_authenticator
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Set the bearer token for authentication
|
|
110
|
+
#
|
|
111
|
+
# @api public
|
|
112
|
+
# @param bearer_token [String] the bearer token for authentication
|
|
113
|
+
# @return [void]
|
|
114
|
+
# @example Set the bearer token
|
|
115
|
+
# client.bearer_token = "new_token"
|
|
116
|
+
def bearer_token=(bearer_token)
|
|
117
|
+
@bearer_token = bearer_token
|
|
118
|
+
initialize_authenticator
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Set the OAuth 2.0 client ID
|
|
122
|
+
#
|
|
123
|
+
# @api public
|
|
124
|
+
# @param client_id [String] the OAuth 2.0 client ID
|
|
125
|
+
# @return [void]
|
|
126
|
+
# @example Set the client ID
|
|
127
|
+
# client.client_id = "new_id"
|
|
128
|
+
def client_id=(client_id)
|
|
129
|
+
@client_id = client_id
|
|
130
|
+
initialize_authenticator
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Set the OAuth 2.0 client secret
|
|
134
|
+
#
|
|
135
|
+
# @api public
|
|
136
|
+
# @param client_secret [String] the OAuth 2.0 client secret
|
|
137
|
+
# @return [void]
|
|
138
|
+
# @example Set the client secret
|
|
139
|
+
# client.client_secret = "new_secret"
|
|
140
|
+
def client_secret=(client_secret)
|
|
141
|
+
@client_secret = client_secret
|
|
142
|
+
initialize_authenticator
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Set the OAuth 2.0 refresh token
|
|
146
|
+
#
|
|
147
|
+
# @api public
|
|
148
|
+
# @param refresh_token [String] the OAuth 2.0 refresh token
|
|
149
|
+
# @return [void]
|
|
150
|
+
# @example Set the refresh token
|
|
151
|
+
# client.refresh_token = "new_token"
|
|
152
|
+
def refresh_token=(refresh_token)
|
|
153
|
+
@refresh_token = refresh_token
|
|
154
|
+
initialize_authenticator
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Initialize credential instance variables
|
|
160
|
+
# @api private
|
|
161
|
+
# @return [void]
|
|
162
|
+
def initialize_credentials(api_key:, api_key_secret:, access_token:, access_token_secret:, bearer_token:,
|
|
163
|
+
client_id:, client_secret:, refresh_token:)
|
|
164
|
+
@api_key = api_key
|
|
165
|
+
@api_key_secret = api_key_secret
|
|
166
|
+
@access_token = access_token
|
|
167
|
+
@access_token_secret = access_token_secret
|
|
168
|
+
@bearer_token = bearer_token
|
|
169
|
+
@client_id = client_id
|
|
170
|
+
@client_secret = client_secret
|
|
171
|
+
@refresh_token = refresh_token
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Initialize the appropriate authenticator based on available credentials
|
|
175
|
+
# @api private
|
|
176
|
+
# @return [Authenticator] the initialized authenticator
|
|
177
|
+
def initialize_authenticator
|
|
178
|
+
@authenticator = oauth_authenticator || oauth2_authenticator || bearer_authenticator || @authenticator || Authenticator.new
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Build an OAuth 1.0a authenticator if credentials are available
|
|
182
|
+
# @api private
|
|
183
|
+
# @return [OAuthAuthenticator, nil] the OAuth authenticator or nil
|
|
184
|
+
def oauth_authenticator
|
|
185
|
+
return unless api_key && api_key_secret && access_token && access_token_secret
|
|
186
|
+
|
|
187
|
+
OAuthAuthenticator.new(api_key:, api_key_secret:, access_token:, access_token_secret:)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Build an OAuth 2.0 authenticator if credentials are available
|
|
191
|
+
# @api private
|
|
192
|
+
# @return [OAuth2Authenticator, nil] the OAuth 2.0 authenticator or nil
|
|
193
|
+
def oauth2_authenticator
|
|
194
|
+
return unless client_id && client_secret && access_token && refresh_token
|
|
195
|
+
|
|
196
|
+
OAuth2Authenticator.new(client_id:, client_secret:, access_token:, refresh_token:)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Build a bearer token authenticator if credentials are available
|
|
200
|
+
# @api private
|
|
201
|
+
# @return [BearerTokenAuthenticator, nil] the bearer token authenticator or nil
|
|
202
|
+
def bearer_authenticator
|
|
203
|
+
return unless bearer_token
|
|
204
|
+
|
|
205
|
+
BearerTokenAuthenticator.new(bearer_token:)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/x/connection.rb
CHANGED
|
@@ -5,15 +5,24 @@ require "uri"
|
|
|
5
5
|
require_relative "errors/network_error"
|
|
6
6
|
|
|
7
7
|
module X
|
|
8
|
+
# Manages HTTP connections to the X API
|
|
9
|
+
# @api public
|
|
8
10
|
class Connection
|
|
9
11
|
extend Forwardable
|
|
10
12
|
|
|
13
|
+
# Default host for the X API
|
|
11
14
|
DEFAULT_HOST = "api.twitter.com".freeze
|
|
15
|
+
# Default port for HTTPS connections
|
|
12
16
|
DEFAULT_PORT = 443
|
|
17
|
+
# Default timeout for opening connections in seconds
|
|
13
18
|
DEFAULT_OPEN_TIMEOUT = 60 # seconds
|
|
19
|
+
# Default timeout for reading responses in seconds
|
|
14
20
|
DEFAULT_READ_TIMEOUT = 60 # seconds
|
|
21
|
+
# Default timeout for writing requests in seconds
|
|
15
22
|
DEFAULT_WRITE_TIMEOUT = 60 # seconds
|
|
23
|
+
# Default debug output destination
|
|
16
24
|
DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
|
|
25
|
+
# Network errors that should be wrapped in NetworkError
|
|
17
26
|
NETWORK_ERRORS = [
|
|
18
27
|
Errno::ECONNREFUSED,
|
|
19
28
|
Errno::ECONNRESET,
|
|
@@ -22,14 +31,66 @@ module X
|
|
|
22
31
|
OpenSSL::SSL::SSLError
|
|
23
32
|
].freeze
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
75
|
|
|
28
76
|
def_delegator :proxy_uri, :host, :proxy_host
|
|
29
77
|
def_delegator :proxy_uri, :port, :proxy_port
|
|
30
78
|
def_delegator :proxy_uri, :user, :proxy_user
|
|
31
79
|
def_delegator :proxy_uri, :password, :proxy_pass
|
|
32
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)
|
|
33
94
|
def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
|
|
34
95
|
write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
|
|
35
96
|
@open_timeout = open_timeout
|
|
@@ -39,6 +100,14 @@ module X
|
|
|
39
100
|
self.proxy_url = proxy_url unless proxy_url.nil?
|
|
40
101
|
end
|
|
41
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)
|
|
42
111
|
def perform(request:)
|
|
43
112
|
host = request.uri.host || DEFAULT_HOST
|
|
44
113
|
port = request.uri.port || DEFAULT_PORT
|
|
@@ -49,6 +118,14 @@ module X
|
|
|
49
118
|
raise NetworkError, "Network error: #{e}"
|
|
50
119
|
end
|
|
51
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"
|
|
52
129
|
def proxy_url=(proxy_url)
|
|
53
130
|
@proxy_url = proxy_url
|
|
54
131
|
proxy_uri = URI(proxy_url)
|
|
@@ -59,6 +136,11 @@ module X
|
|
|
59
136
|
|
|
60
137
|
private
|
|
61
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
|
|
62
144
|
def build_http_client(host = DEFAULT_HOST, port = DEFAULT_PORT)
|
|
63
145
|
http_client = if proxy_uri
|
|
64
146
|
Net::HTTP.new(host, port, proxy_host, proxy_port, proxy_user, proxy_pass)
|
|
@@ -68,6 +150,10 @@ module X
|
|
|
68
150
|
configure_http_client(http_client)
|
|
69
151
|
end
|
|
70
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
|
|
71
157
|
def configure_http_client(http_client)
|
|
72
158
|
http_client.tap do |c|
|
|
73
159
|
c.open_timeout = open_timeout
|
data/lib/x/errors/bad_gateway.rb
CHANGED
data/lib/x/errors/bad_request.rb
CHANGED
data/lib/x/errors/error.rb
CHANGED
data/lib/x/errors/forbidden.rb
CHANGED
data/lib/x/errors/gone.rb
CHANGED
data/lib/x/errors/http_error.rb
CHANGED
|
@@ -2,17 +2,46 @@ require "json"
|
|
|
2
2
|
require_relative "error"
|
|
3
3
|
|
|
4
4
|
module X
|
|
5
|
+
# Base class for HTTP errors from the X API
|
|
6
|
+
# @api public
|
|
5
7
|
class HTTPError < Error
|
|
8
|
+
# Regular expression to match JSON content types
|
|
6
9
|
JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
|
|
7
10
|
|
|
8
|
-
|
|
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
|
|
9
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)
|
|
10
32
|
def initialize(response:)
|
|
11
33
|
super(error_message(response))
|
|
12
34
|
@response = response
|
|
13
35
|
@code = response.code
|
|
14
36
|
end
|
|
15
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)
|
|
16
45
|
def error_message(response)
|
|
17
46
|
if json?(response)
|
|
18
47
|
message_from_json_response(response)
|
|
@@ -21,19 +50,33 @@ module X
|
|
|
21
50
|
end
|
|
22
51
|
end
|
|
23
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)
|
|
24
60
|
def message_from_json_response(response)
|
|
25
61
|
response_object = JSON.parse(response.body)
|
|
26
|
-
if response_object
|
|
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")
|
|
27
65
|
"#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
|
|
28
66
|
elsif response_object.key?("error")
|
|
29
67
|
response_object.fetch("error")
|
|
30
|
-
elsif response_object["errors"].instance_of?(Array)
|
|
31
|
-
response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
|
|
32
68
|
else
|
|
33
69
|
response.message
|
|
34
70
|
end
|
|
35
71
|
end
|
|
36
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)
|
|
37
80
|
def json?(response)
|
|
38
81
|
JSON_CONTENT_TYPE_REGEXP === response["content-type"]
|
|
39
82
|
end
|
data/lib/x/errors/not_found.rb
CHANGED
|
@@ -2,25 +2,57 @@ require_relative "client_error"
|
|
|
2
2
|
require_relative "../rate_limit"
|
|
3
3
|
|
|
4
4
|
module X
|
|
5
|
+
# Error raised when rate limit is exceeded (HTTP 429)
|
|
6
|
+
# @api public
|
|
5
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
|
|
6
14
|
def rate_limit
|
|
7
15
|
rate_limits.max_by(&:reset_at)
|
|
8
16
|
end
|
|
9
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
|
|
10
24
|
def rate_limits
|
|
11
25
|
@rate_limits ||= RateLimit::TYPES.filter_map do |type|
|
|
12
26
|
RateLimit.new(type:, response:) if response["x-#{type}-remaining"].eql?("0")
|
|
13
27
|
end
|
|
14
28
|
end
|
|
15
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
|
|
16
36
|
def reset_at
|
|
17
37
|
rate_limit&.reset_at || Time.at(0)
|
|
18
38
|
end
|
|
19
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
|
|
20
46
|
def reset_in
|
|
21
47
|
[(reset_at - Time.now).ceil, 0].max
|
|
22
48
|
end
|
|
23
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
|
|
24
56
|
alias_method :retry_after, :reset_in
|
|
25
57
|
end
|
|
26
58
|
end
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
module X
|
|
2
|
+
# Validates media upload parameters
|
|
3
|
+
# @api public
|
|
2
4
|
module MediaUploadValidator
|
|
3
5
|
module_function
|
|
4
6
|
|
|
7
|
+
# Valid media category values
|
|
5
8
|
MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
|
|
6
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")
|
|
7
18
|
def validate_file_path!(file_path:)
|
|
8
19
|
raise "File not found: #{file_path}" unless File.exist?(file_path)
|
|
9
20
|
end
|
|
10
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")
|
|
11
30
|
def validate_media_category!(media_category:)
|
|
12
31
|
return if MEDIA_CATEGORIES.include?(media_category.downcase)
|
|
13
32
|
|