x 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +35 -3
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -0
- data/README.md +50 -17
- data/Rakefile +2 -1
- data/lib/x/authenticator.rb +43 -0
- data/lib/x/client.rb +58 -160
- data/lib/x/client_defaults.rb +13 -0
- data/lib/x/connection.rb +21 -0
- data/lib/x/errors/authentication_error.rb +5 -0
- data/lib/x/errors/bad_request_error.rb +5 -0
- data/lib/x/errors/client_error.rb +5 -0
- data/lib/x/errors/error.rb +20 -0
- data/lib/x/errors/errors.rb +27 -0
- data/lib/x/errors/forbidden_error.rb +5 -0
- data/lib/x/errors/network_error.rb +5 -0
- data/lib/x/errors/not_found_error.rb +5 -0
- data/lib/x/errors/server_error.rb +5 -0
- data/lib/x/errors/service_unavailable_error.rb +5 -0
- data/lib/x/errors/too_many_requests_error.rb +30 -0
- data/lib/x/request_builder.rb +29 -0
- data/lib/x/response_handler.rb +34 -0
- data/lib/x/version.rb +37 -1
- data/lib/x.rb +0 -2
- metadata +20 -5
- data/lib/x/errors.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e5f85bad7fca01ea015e9a7acb13c6652ad6d06f651acacba1609f5299045ab
|
4
|
+
data.tar.gz: e10427f17c76a569c5bea2c6f50cc71d812ea9987db1c288db2e319d976760c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5ab83ea7ee748d3e17c43bc5f7ce55a59685e7d4e10039c5e7cc28f0bca7b31c689e868f763c5f84b7ffcc9ea92f850b578cc14fede2c29cd24e73cad4c4548
|
7
|
+
data.tar.gz: 01cf440dbad6c968d4cf8b83b4ac496d03309904c9d2c2b1aa01a0bd8d2894a5bfec1990ec7a01de488098b69917e82216cd1d53828c3d594ab49a7a6436e6b2
|
data/.rubocop.yml
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require:
|
2
|
+
- standard
|
3
|
+
- standard-performance
|
2
4
|
- rubocop-minitest
|
3
5
|
- rubocop-performance
|
4
6
|
- rubocop-rake
|
@@ -7,6 +9,39 @@ AllCops:
|
|
7
9
|
NewCops: enable
|
8
10
|
TargetRubyVersion: 3.0
|
9
11
|
|
12
|
+
Layout/ArgumentAlignment:
|
13
|
+
Enabled: true
|
14
|
+
EnforcedStyle: with_fixed_indentation
|
15
|
+
|
16
|
+
Layout/ArrayAlignment:
|
17
|
+
Enabled: true
|
18
|
+
EnforcedStyle: with_fixed_indentation
|
19
|
+
|
20
|
+
Layout/EndAlignment:
|
21
|
+
Enabled: true
|
22
|
+
EnforcedStyleAlignWith: variable
|
23
|
+
|
24
|
+
Layout/HashAlignment:
|
25
|
+
Enabled: true
|
26
|
+
EnforcedHashRocketStyle: key
|
27
|
+
EnforcedColonStyle: key
|
28
|
+
EnforcedLastArgumentHashStyle: always_inspect
|
29
|
+
|
30
|
+
Layout/ParameterAlignment:
|
31
|
+
Enabled: true
|
32
|
+
EnforcedStyle: with_fixed_indentation
|
33
|
+
IndentationWidth: ~
|
34
|
+
|
35
|
+
Layout/SpaceInsideHashLiteralBraces:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
Metrics/ParameterLists:
|
39
|
+
CountKeywordArgs: false
|
40
|
+
|
41
|
+
Style/Alias:
|
42
|
+
Enabled: true
|
43
|
+
EnforcedStyle: prefer_alias_method
|
44
|
+
|
10
45
|
Style/StringLiterals:
|
11
46
|
Enabled: true
|
12
47
|
EnforcedStyle: double_quotes
|
@@ -17,6 +52,3 @@ Style/StringLiteralsInInterpolation:
|
|
17
52
|
|
18
53
|
Style/FrozenStringLiteralComment:
|
19
54
|
Enabled: false
|
20
|
-
|
21
|
-
Metrics/ParameterLists:
|
22
|
-
CountKeywordArgs: false
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0] - 2023-08-06
|
4
|
+
|
5
|
+
- Refactor Client into Authenticator, RequestBuilder, Connection, ResponseHandler (6bee1e9)
|
6
|
+
- Add configurable open timeout (1000f9d)
|
7
|
+
- Allow configuration of content type (f33a732)
|
8
|
+
|
9
|
+
## [0.3.0] - 2023-08-04
|
10
|
+
|
11
|
+
- Add accessors to X::Client (e61fa73)
|
12
|
+
- Add configurable read timeout (41502b9)
|
13
|
+
- Handle network-related errors (9ed1fb4)
|
14
|
+
- Include response body in errors (a203e6a)
|
15
|
+
|
3
16
|
## [0.2.0] - 2023-08-02
|
4
17
|
|
5
18
|
- Allow configuration of base URL (4bc0531)
|
data/Gemfile
CHANGED
@@ -3,6 +3,7 @@ source "https://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in x.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
+
gem "hashie", ">= 5"
|
6
7
|
gem "minitest", ">= 5.19"
|
7
8
|
gem "rake", ">= 13.0.6"
|
8
9
|
gem "rubocop", ">= 1.21"
|
@@ -10,4 +11,5 @@ gem "rubocop-minitest", ">= 0.31"
|
|
10
11
|
gem "rubocop-performance", ">= 1.18"
|
11
12
|
gem "rubocop-rake", ">= 0.6"
|
12
13
|
gem "simplecov", ">= 0.22"
|
14
|
+
gem "standard", ">= 1.30.1"
|
13
15
|
gem "webmock", ">= 3.18.1"
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# X
|
2
2
|
|
3
|
-
A Ruby interface to the X
|
3
|
+
A Ruby interface to the X API.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -15,24 +15,57 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
15
15
|
## Usage
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
18
|
+
x_oauth_credentials = {
|
19
|
+
api_key: "INSERT YOUR X API KEY HERE",
|
20
|
+
api_key_secret: "INSERT YOUR X API KEY SECRET HERE",
|
21
|
+
access_token: "INSERT YOUR X API ACCESS TOKEN HERE",
|
22
|
+
access_token_secret: "INSERT YOUR X API ACCESS TOKEN SECRET HERE",
|
23
|
+
}
|
24
|
+
|
25
|
+
# Initialize X API client with OAuth credentials
|
26
|
+
x_client = X::Client.new(**x_oauth_credentials)
|
27
|
+
|
28
|
+
# Request yourself
|
29
|
+
x_client.get("users/me")
|
30
|
+
# {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
|
31
|
+
|
32
|
+
# Post a tweet
|
33
|
+
tweet = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
|
34
|
+
# {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
|
35
|
+
|
36
|
+
# Delete a tweet
|
37
|
+
x_client.delete("tweets/#{tweet["data"]["id"]}")
|
38
|
+
# {"data"=>{"deleted"=>true}}
|
39
|
+
|
40
|
+
# Initialize an API v1.1 client
|
41
|
+
v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_oauth_credentials)
|
42
|
+
|
43
|
+
# Request your account settings
|
44
|
+
v1_client.get("account/settings.json")
|
45
|
+
|
46
|
+
# Initialize an X Ads API client
|
47
|
+
ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_oauth_credentials)
|
48
|
+
|
49
|
+
# Request your ad accounts
|
50
|
+
ads_client.get("accounts")
|
34
51
|
```
|
35
52
|
|
53
|
+
## History and Philosophy
|
54
|
+
|
55
|
+
This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests). At the time of writing, this library is about 200 lines of code (plus 200 test lines) and I’d like to keep it that way. That doesn’t mean new features won’t be added over time, but the benefits of potential new features must be weighed against the benefits of simplicity:
|
56
|
+
|
57
|
+
* Less code is easier to maintain.
|
58
|
+
* Less code means fewer bugs.
|
59
|
+
* Less code runs faster.
|
60
|
+
|
61
|
+
In the immortal words of [Ezra Zygmuntowicz](https://github.com/ezmobius) and his [Merb](https://github.com/merb) project (may they both rest in peace): “No code is faster than no code.” The fastest code is the code that is never executed because it doesn’t exist. That principle should apply not just to this library itself but to third-party dependencies. At present, this library has one dependency ([oauth](https://rubygems.org/gems/oauth)) and I’d like to keep it that way. If anything, it should have fewer.
|
62
|
+
|
63
|
+
The tests for the previous version of this library ran in about 2 seconds. That sounds pretty fast until you see that tests for this library run in 2 hundredths of a second. This means you can automatically run the tests any time you write a file and receive immediate feedback. For such of workflows, 2 seconds feels painfully slow. At the same time, we aim to maintain 100% C0 code coverage.
|
64
|
+
|
65
|
+
This code is not littered with comments that are intended to generate documentation. Rather, this code is intended to be simple enough to serve as its own documentation. If you want to understand how something works, don’t read the documentation—it might be wrong—just read the code. The code is always right.
|
66
|
+
|
67
|
+
This project conforms to [Standard Ruby](https://github.com/standardrb/standard). Patches that don’t maintain that standard will not be accepted.
|
68
|
+
|
36
69
|
## Development
|
37
70
|
|
38
71
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "oauth"
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
module X
|
5
|
+
# Handles OAuth and bearer token authentication
|
6
|
+
class Authenticator
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_accessor :bearer_token
|
10
|
+
|
11
|
+
def_delegator :@access_token, :secret, :access_token_secret
|
12
|
+
def_delegator :@access_token, :secret=, :access_token_secret=
|
13
|
+
def_delegator :@access_token, :token, :access_token
|
14
|
+
def_delegator :@access_token, :token=, :access_token=
|
15
|
+
def_delegator :@consumer, :key, :api_key
|
16
|
+
def_delegator :@consumer, :key=, :api_key=
|
17
|
+
def_delegator :@consumer, :secret, :api_key_secret
|
18
|
+
def_delegator :@consumer, :secret=, :api_key_secret=
|
19
|
+
|
20
|
+
def initialize(bearer_token:, api_key:, api_key_secret:, access_token:, access_token_secret:)
|
21
|
+
if bearer_token
|
22
|
+
@bearer_token = bearer_token
|
23
|
+
else
|
24
|
+
initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def sign!(request)
|
29
|
+
@consumer.sign!(request, @access_token)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
35
|
+
unless api_key && api_key_secret && access_token && access_token_secret
|
36
|
+
raise ArgumentError, "Missing OAuth credentials"
|
37
|
+
end
|
38
|
+
|
39
|
+
@consumer = OAuth::Consumer.new(api_key, api_key_secret, site: ClientDefaults::DEFAULT_BASE_URL)
|
40
|
+
@access_token = OAuth::Token.new(access_token, access_token_secret)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/x/client.rb
CHANGED
@@ -1,194 +1,92 @@
|
|
1
|
-
require "
|
2
|
-
|
3
|
-
|
1
|
+
require "forwardable"
|
2
|
+
require_relative "authenticator"
|
3
|
+
require_relative "client_defaults"
|
4
|
+
require_relative "connection"
|
5
|
+
require_relative "request_builder"
|
6
|
+
require_relative "response_handler"
|
4
7
|
|
5
8
|
module X
|
6
|
-
#
|
9
|
+
# Main public interface
|
7
10
|
class Client
|
8
|
-
|
9
|
-
|
11
|
+
extend Forwardable
|
12
|
+
include ClientDefaults
|
13
|
+
|
14
|
+
attr_reader :base_url
|
15
|
+
attr_accessor :content_type, :open_timeout, :read_timeout, :user_agent, :array_class, :object_class
|
16
|
+
|
17
|
+
def_delegators :@authenticator, :bearer_token, :api_key, :api_key_secret, :access_token, :access_token_secret
|
18
|
+
def_delegators :@authenticator, :bearer_token=, :api_key=, :api_key_secret=, :access_token=, :access_token_secret=
|
10
19
|
|
11
20
|
def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
21
|
+
base_url: DEFAULT_BASE_URL, content_type: DEFAULT_CONTENT_TYPE,
|
22
|
+
open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, user_agent: DEFAULT_USER_AGENT,
|
23
|
+
array_class: DEFAULT_ARRAY_CLASS, object_class: DEFAULT_OBJECT_CLASS)
|
24
|
+
|
25
|
+
@authenticator = Authenticator.new(bearer_token: bearer_token, api_key: api_key, api_key_secret: api_key_secret,
|
26
|
+
access_token: access_token, access_token_secret: access_token_secret)
|
27
|
+
self.base_url = base_url
|
28
|
+
@content_type = content_type
|
29
|
+
@open_timeout = open_timeout
|
30
|
+
@read_timeout = read_timeout
|
31
|
+
@user_agent = user_agent
|
32
|
+
@array_class = array_class
|
33
|
+
@object_class = object_class
|
20
34
|
end
|
21
35
|
|
22
36
|
def get(endpoint)
|
23
|
-
|
37
|
+
send_request(:get, endpoint)
|
24
38
|
end
|
25
39
|
|
26
40
|
def post(endpoint, body = nil)
|
27
|
-
|
41
|
+
send_request(:post, endpoint, body)
|
28
42
|
end
|
29
43
|
|
30
44
|
def put(endpoint, body = nil)
|
31
|
-
|
45
|
+
send_request(:put, endpoint, body)
|
32
46
|
end
|
33
47
|
|
34
48
|
def delete(endpoint)
|
35
|
-
|
49
|
+
send_request(:delete, endpoint)
|
36
50
|
end
|
37
51
|
|
38
|
-
|
52
|
+
def base_url=(new_base_url)
|
53
|
+
uri = URI(new_base_url)
|
54
|
+
raise ArgumentError, "Invalid base URL" unless uri.is_a?(URI::HTTPS) || uri.is_a?(URI::HTTP)
|
39
55
|
|
40
|
-
|
41
|
-
response = yield
|
42
|
-
ErrorHandler.new(response).handle
|
56
|
+
@base_url = uri
|
43
57
|
end
|
44
58
|
|
45
|
-
|
46
|
-
class HttpRequest
|
47
|
-
HTTP_METHODS = {
|
48
|
-
get: Net::HTTP::Get,
|
49
|
-
post: Net::HTTP::Post,
|
50
|
-
put: Net::HTTP::Put,
|
51
|
-
delete: Net::HTTP::Delete
|
52
|
-
}.freeze
|
53
|
-
|
54
|
-
def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
55
|
-
base_url: nil, user_agent: nil)
|
56
|
-
@base_url = base_url
|
57
|
-
@use_bearer_token = !bearer_token.nil?
|
58
|
-
@user_agent = user_agent || Client::DEFAULT_USER_AGENT
|
59
|
-
|
60
|
-
if @use_bearer_token
|
61
|
-
@bearer_token = bearer_token
|
62
|
-
else
|
63
|
-
initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def get(endpoint)
|
68
|
-
send_request(:get, endpoint)
|
69
|
-
end
|
70
|
-
|
71
|
-
def post(endpoint, body = nil)
|
72
|
-
send_request(:post, endpoint, body)
|
73
|
-
end
|
74
|
-
|
75
|
-
def put(endpoint, body = nil)
|
76
|
-
send_request(:put, endpoint, body)
|
77
|
-
end
|
78
|
-
|
79
|
-
def delete(endpoint)
|
80
|
-
send_request(:delete, endpoint)
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
|
85
|
-
def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
86
|
-
unless api_key && api_key_secret && access_token && access_token_secret
|
87
|
-
raise ArgumentError, "Missing OAuth credentials."
|
88
|
-
end
|
89
|
-
|
90
|
-
@consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
|
91
|
-
@access_token = OAuth::Token.new(access_token, access_token_secret)
|
92
|
-
end
|
93
|
-
|
94
|
-
def send_request(http_method, endpoint, body = nil)
|
95
|
-
url = URI.parse(@base_url + endpoint)
|
96
|
-
http = Net::HTTP.new(url.host, url.port)
|
97
|
-
http.use_ssl = true
|
98
|
-
|
99
|
-
request = create_request(http_method, url, body)
|
100
|
-
add_authorization(request)
|
101
|
-
add_user_agent(request)
|
102
|
-
|
103
|
-
http.request(request)
|
104
|
-
end
|
105
|
-
|
106
|
-
def create_request(http_method, url, body)
|
107
|
-
http_method_class = HTTP_METHODS[http_method]
|
59
|
+
private
|
108
60
|
|
109
|
-
|
61
|
+
def send_request(http_method, endpoint, body = nil)
|
62
|
+
request = RequestBuilder.build(http_method, @base_url, endpoint, body)
|
63
|
+
add_headers(request)
|
110
64
|
|
111
|
-
|
112
|
-
request.body = body if body && http_method != :get
|
113
|
-
request
|
114
|
-
end
|
65
|
+
response = Connection.send_request(@base_url, @open_timeout, @read_timeout, request)
|
115
66
|
|
116
|
-
|
117
|
-
if @use_bearer_token
|
118
|
-
request["Authorization"] = "Bearer #{@bearer_token}"
|
119
|
-
else
|
120
|
-
@consumer.sign!(request, @access_token)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def add_user_agent(request)
|
125
|
-
request["User-Agent"] = @user_agent if @user_agent
|
126
|
-
end
|
67
|
+
ResponseHandler.new(response, @array_class, @object_class).handle
|
127
68
|
end
|
128
69
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
Net::HTTPForbidden => :handle_forbidden_response,
|
135
|
-
Net::HTTPUnauthorized => :handle_unauthorized_response,
|
136
|
-
Net::HTTPNotFound => :handle_not_found_response,
|
137
|
-
Net::HTTPTooManyRequests => :handle_too_many_requests_response,
|
138
|
-
Net::HTTPInternalServerError => :handle_server_error_response,
|
139
|
-
Net::HTTPServiceUnavailable => :handle_service_unavailable_response
|
140
|
-
}.freeze
|
141
|
-
|
142
|
-
def initialize(response)
|
143
|
-
@response = response
|
144
|
-
end
|
145
|
-
|
146
|
-
def handle
|
147
|
-
handler_method = HTTP_STATUS_HANDLERS[@response.class]
|
148
|
-
if handler_method
|
149
|
-
send(handler_method)
|
150
|
-
else
|
151
|
-
handle_unexpected_response
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
private
|
156
|
-
|
157
|
-
def handle_success_response
|
158
|
-
JSON.parse(@response.body)
|
159
|
-
end
|
160
|
-
|
161
|
-
def handle_bad_request_response
|
162
|
-
raise X::BadRequestError, "Bad request: #{@response.code} #{@response.message}"
|
163
|
-
end
|
164
|
-
|
165
|
-
def handle_forbidden_response
|
166
|
-
raise X::ForbiddenError, "Forbidden: #{@response.code} #{@response.message}"
|
167
|
-
end
|
168
|
-
|
169
|
-
def handle_unauthorized_response
|
170
|
-
raise X::AuthenticationError, "Authentication failed. Please check your credentials."
|
171
|
-
end
|
172
|
-
|
173
|
-
def handle_not_found_response
|
174
|
-
raise X::NotFoundError, "Not found: #{@response.code} #{@response.message}"
|
175
|
-
end
|
176
|
-
|
177
|
-
def handle_too_many_requests_response
|
178
|
-
raise X::TooManyRequestsError, "Too many requests: #{@response.code} #{@response.message}"
|
179
|
-
end
|
70
|
+
def add_headers(request)
|
71
|
+
add_authorization(request)
|
72
|
+
add_content_type(request)
|
73
|
+
add_user_agent(request)
|
74
|
+
end
|
180
75
|
|
181
|
-
|
182
|
-
|
76
|
+
def add_authorization(request)
|
77
|
+
if @authenticator.bearer_token
|
78
|
+
request["Authorization"] = "Bearer #{@bearer_token}"
|
79
|
+
else
|
80
|
+
@authenticator.sign!(request)
|
183
81
|
end
|
82
|
+
end
|
184
83
|
|
185
|
-
|
186
|
-
|
187
|
-
|
84
|
+
def add_content_type(request)
|
85
|
+
request["Content-Type"] = @content_type if @content_type
|
86
|
+
end
|
188
87
|
|
189
|
-
|
190
|
-
|
191
|
-
end
|
88
|
+
def add_user_agent(request)
|
89
|
+
request["User-Agent"] = @user_agent if @user_agent
|
192
90
|
end
|
193
91
|
end
|
194
92
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
|
3
|
+
module X
|
4
|
+
module ClientDefaults
|
5
|
+
DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
|
6
|
+
DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8".freeze
|
7
|
+
DEFAULT_ARRAY_CLASS = Array
|
8
|
+
DEFAULT_OBJECT_CLASS = Hash
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 60 # seconds
|
10
|
+
DEFAULT_READ_TIMEOUT = 60 # seconds
|
11
|
+
DEFAULT_USER_AGENT = "X-Client/#{Version} Ruby/#{RUBY_VERSION}".freeze
|
12
|
+
end
|
13
|
+
end
|
data/lib/x/connection.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require_relative "errors/network_error"
|
3
|
+
require_relative "errors/errors"
|
4
|
+
|
5
|
+
module X
|
6
|
+
# Sends HTTP requests
|
7
|
+
class Connection
|
8
|
+
include Errors
|
9
|
+
|
10
|
+
def self.send_request(base_url, open_timeout, read_timeout, request)
|
11
|
+
url = URI(base_url)
|
12
|
+
http = Net::HTTP.new(url.host, url.port)
|
13
|
+
http.use_ssl = url.scheme == "https"
|
14
|
+
http.open_timeout = open_timeout
|
15
|
+
http.read_timeout = read_timeout
|
16
|
+
http.request(request)
|
17
|
+
rescue *NETWORK_ERRORS => e
|
18
|
+
raise NetworkError, "Network error: #{e.message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module X
|
4
|
+
# Base error class
|
5
|
+
class Error < ::StandardError
|
6
|
+
include ClientDefaults
|
7
|
+
attr_reader :object
|
8
|
+
|
9
|
+
def initialize(msg = nil, response = nil, object_class = DEFAULT_OBJECT_CLASS)
|
10
|
+
@object = JSON.parse(response.body, object_class: object_class) if json_response?(response)
|
11
|
+
super(msg)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def json_response?(response)
|
17
|
+
response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == DEFAULT_CONTENT_TYPE
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "bad_request_error"
|
2
|
+
require_relative "authentication_error"
|
3
|
+
require_relative "forbidden_error"
|
4
|
+
require_relative "not_found_error"
|
5
|
+
require_relative "too_many_requests_error"
|
6
|
+
require_relative "server_error"
|
7
|
+
require_relative "service_unavailable_error"
|
8
|
+
|
9
|
+
module X
|
10
|
+
module Errors
|
11
|
+
ERROR_CLASSES = {
|
12
|
+
400 => BadRequestError,
|
13
|
+
401 => AuthenticationError,
|
14
|
+
403 => ForbiddenError,
|
15
|
+
404 => NotFoundError,
|
16
|
+
429 => TooManyRequestsError,
|
17
|
+
500 => ServerError,
|
18
|
+
503 => ServiceUnavailableError
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
NETWORK_ERRORS = [
|
22
|
+
Errno::ECONNREFUSED,
|
23
|
+
Net::OpenTimeout,
|
24
|
+
Net::ReadTimeout
|
25
|
+
].freeze
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require_relative "client_error"
|
3
|
+
|
4
|
+
module X
|
5
|
+
# Rate limit error
|
6
|
+
class TooManyRequestsError < ClientError
|
7
|
+
def initialize(msg, response = nil, object_class = ClientDefaults::DEFAULT_OBJECT_CLASS)
|
8
|
+
@response = response
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def limit
|
13
|
+
@response&.fetch("x-rate-limit-limit", 0).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def remaining
|
17
|
+
@response&.fetch("x-rate-limit-remaining", 0).to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset_at
|
21
|
+
Time.at(@response&.fetch("x-rate-limit-reset", 0).to_i).utc if @response
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset_in
|
25
|
+
[(reset_at - Time.now).ceil, 0].max if reset_at
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :retry_after, :reset_in
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module X
|
5
|
+
# Creates HTTP requests
|
6
|
+
class RequestBuilder
|
7
|
+
HTTP_METHODS = {
|
8
|
+
get: Net::HTTP::Get,
|
9
|
+
post: Net::HTTP::Post,
|
10
|
+
put: Net::HTTP::Put,
|
11
|
+
delete: Net::HTTP::Delete
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def self.build(http_method, base_url, endpoint, body = nil)
|
15
|
+
url = URI.join(base_url, endpoint)
|
16
|
+
create_request(http_method, url, body)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create_request(http_method, url, body)
|
20
|
+
http_method_class = HTTP_METHODS[http_method]
|
21
|
+
|
22
|
+
raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
|
23
|
+
|
24
|
+
request = http_method_class.new(url)
|
25
|
+
request.body = body if body && http_method != :get
|
26
|
+
request
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative "errors/errors"
|
3
|
+
|
4
|
+
module X
|
5
|
+
# Process HTTP responses
|
6
|
+
class ResponseHandler
|
7
|
+
include ClientDefaults
|
8
|
+
include Errors
|
9
|
+
|
10
|
+
def initialize(response, array_class, object_class)
|
11
|
+
@response = response
|
12
|
+
@array_class = array_class
|
13
|
+
@object_class = object_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle
|
17
|
+
if successful_json_response?
|
18
|
+
return JSON.parse(@response.body, array_class: @array_class, object_class: @object_class)
|
19
|
+
end
|
20
|
+
|
21
|
+
error_class = ERROR_CLASSES[@response.code.to_i] || Error
|
22
|
+
error_message = "#{@response.code} #{@response.message}"
|
23
|
+
raise error_class, error_message if @response.body.nil? || @response.body.empty?
|
24
|
+
|
25
|
+
raise error_class.new(error_message, @response)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def successful_json_response?
|
31
|
+
@response.is_a?(Net::HTTPSuccess) && @response.body && @response["content-type"] == DEFAULT_CONTENT_TYPE
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/x/version.rb
CHANGED
@@ -1,3 +1,39 @@
|
|
1
1
|
module X
|
2
|
-
|
2
|
+
# The version of this library
|
3
|
+
module Version
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def major
|
7
|
+
0
|
8
|
+
end
|
9
|
+
|
10
|
+
def minor
|
11
|
+
4
|
12
|
+
end
|
13
|
+
|
14
|
+
def patch
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
def pre
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
major: major,
|
25
|
+
minor: minor,
|
26
|
+
patch: patch,
|
27
|
+
pre: pre
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
[major, minor, patch, pre].compact
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
to_a.join(".")
|
37
|
+
end
|
38
|
+
end
|
3
39
|
end
|
data/lib/x.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: x
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Erik Berlin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-08-
|
11
|
+
date: 2023-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oauth
|
@@ -38,8 +38,23 @@ files:
|
|
38
38
|
- README.md
|
39
39
|
- Rakefile
|
40
40
|
- lib/x.rb
|
41
|
+
- lib/x/authenticator.rb
|
41
42
|
- lib/x/client.rb
|
42
|
-
- lib/x/
|
43
|
+
- lib/x/client_defaults.rb
|
44
|
+
- lib/x/connection.rb
|
45
|
+
- lib/x/errors/authentication_error.rb
|
46
|
+
- lib/x/errors/bad_request_error.rb
|
47
|
+
- lib/x/errors/client_error.rb
|
48
|
+
- lib/x/errors/error.rb
|
49
|
+
- lib/x/errors/errors.rb
|
50
|
+
- lib/x/errors/forbidden_error.rb
|
51
|
+
- lib/x/errors/network_error.rb
|
52
|
+
- lib/x/errors/not_found_error.rb
|
53
|
+
- lib/x/errors/server_error.rb
|
54
|
+
- lib/x/errors/service_unavailable_error.rb
|
55
|
+
- lib/x/errors/too_many_requests_error.rb
|
56
|
+
- lib/x/request_builder.rb
|
57
|
+
- lib/x/response_handler.rb
|
43
58
|
- lib/x/version.rb
|
44
59
|
- sig/x.rbs
|
45
60
|
homepage: https://github.com/sferik/x-ruby
|
@@ -66,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
81
|
- !ruby/object:Gem::Version
|
67
82
|
version: '0'
|
68
83
|
requirements: []
|
69
|
-
rubygems_version: 3.4.
|
84
|
+
rubygems_version: 3.4.18
|
70
85
|
signing_key:
|
71
86
|
specification_version: 4
|
72
|
-
summary: A Ruby interface to the X
|
87
|
+
summary: A Ruby interface to the X API.
|
73
88
|
test_files: []
|
data/lib/x/errors.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
module X
|
2
|
-
class Error < ::StandardError; end
|
3
|
-
class ClientError < Error; end
|
4
|
-
class AuthenticationError < ClientError; end
|
5
|
-
class BadRequestError < ClientError; end
|
6
|
-
class ForbiddenError < ClientError; end
|
7
|
-
class NotFoundError < ClientError; end
|
8
|
-
class TooManyRequestsError < ClientError; end
|
9
|
-
class ServerError < Error; end
|
10
|
-
class ServiceUnavailableError < ServerError; end
|
11
|
-
end
|