x 0.1.0 → 0.3.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/.rubocop.yml +3 -3
- data/CHANGELOG.md +10 -0
- data/Gemfile +1 -0
- data/README.md +33 -13
- data/lib/x/client.rb +157 -37
- data/lib/x/version.rb +37 -1
- data/lib/x.rb +0 -2
- metadata +2 -3
- data/lib/x/error.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56ad55c544f1c7774395d906e0d9d07b6dc2a6d20e37d7df0b3e5b9f7b7b2bb6
|
|
4
|
+
data.tar.gz: 1747e256413ee2aeae02d8ed0e351b6d411217b00e634a42aeec5299764d6fac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5b386cd7386457b148c555359e68c4f9cb72b598ff9eb0a75405f3af75599dca0d5fc1700b01653465b2e50e401b69e9f9e5e9dd126d6a56c46b6df96b5736c
|
|
7
|
+
data.tar.gz: 462957aeee740d46f4a83497e69bec852fa0a4d8a9e38aec6bf8ffcd8f432e862737fa34847995dce5a49c09a40d9a08607ace68a1314a32972292d881cbd1db
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Add accessors to X::Client (e61fa73)
|
|
4
|
+
- Add configurable read timeout (41502b9)
|
|
5
|
+
- Handle network-related errors (9ed1fb4)
|
|
6
|
+
- Include response body in errors (a203e6a)
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2023-08-02
|
|
9
|
+
|
|
10
|
+
- Allow configuration of base URL (4bc0531)
|
|
11
|
+
- Improve error handling (14dc0cd)
|
|
12
|
+
|
|
3
13
|
## [0.1.0] - 2023-08-02
|
|
4
14
|
|
|
5
15
|
- Initial release
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -15,19 +15,39 @@ 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
|
-
|
|
18
|
+
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(**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/", **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/", **oauth_credentials)
|
|
48
|
+
|
|
49
|
+
# Request your ad accounts
|
|
50
|
+
ads_client.get("accounts")
|
|
31
51
|
```
|
|
32
52
|
|
|
33
53
|
## Development
|
data/lib/x/client.rb
CHANGED
|
@@ -1,11 +1,80 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "forwardable"
|
|
2
2
|
require "json"
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "oauth"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "version"
|
|
4
7
|
|
|
5
8
|
module X
|
|
6
|
-
|
|
9
|
+
JSON_CONTENT_TYPE = "application/json; charset=utf-8".freeze
|
|
10
|
+
|
|
11
|
+
# Base error class
|
|
12
|
+
class Error < ::StandardError
|
|
13
|
+
attr_reader :object
|
|
14
|
+
|
|
15
|
+
def initialize(response = nil)
|
|
16
|
+
if response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == JSON_CONTENT_TYPE
|
|
17
|
+
@object = JSON.parse(response.body)
|
|
18
|
+
end
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class NetworkError < Error; end
|
|
24
|
+
class ClientError < Error; end
|
|
25
|
+
class AuthenticationError < ClientError; end
|
|
26
|
+
class BadRequestError < ClientError; end
|
|
27
|
+
class ForbiddenError < ClientError; end
|
|
28
|
+
class NotFoundError < ClientError; end
|
|
29
|
+
|
|
30
|
+
# Rate limit error
|
|
31
|
+
class TooManyRequestsError < ClientError
|
|
32
|
+
def initialize(response = nil)
|
|
33
|
+
@response = response
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def limit
|
|
38
|
+
@response && @response["x-rate-limit-limit"]&.to_i
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remaining
|
|
42
|
+
@response && @response["x-rate-limit-remaining"]&.to_i
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset_at
|
|
46
|
+
reset = @response && @response["x-rate-limit-reset"]&.to_i
|
|
47
|
+
Time.at(reset.to_i).utc if reset
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reset_in
|
|
51
|
+
[(reset_at - Time.now).ceil, 0].max if reset_at
|
|
52
|
+
end
|
|
53
|
+
alias retry_after reset_in
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class ServerError < Error; end
|
|
57
|
+
class ServiceUnavailableError < ServerError; end
|
|
58
|
+
|
|
59
|
+
# HTTP client that handles authentication and requests
|
|
7
60
|
class Client
|
|
8
|
-
|
|
61
|
+
extend Forwardable
|
|
62
|
+
|
|
63
|
+
attr_accessor :bearer_token, :user_agent, :read_timeout
|
|
64
|
+
attr_reader :base_url
|
|
65
|
+
|
|
66
|
+
def_delegator :@access_token, :secret, :access_token_secret
|
|
67
|
+
def_delegator :@access_token, :secret=, :access_token_secret=
|
|
68
|
+
def_delegator :@access_token, :token, :access_token
|
|
69
|
+
def_delegator :@access_token, :token=, :access_token=
|
|
70
|
+
def_delegator :@consumer, :key, :api_key
|
|
71
|
+
def_delegator :@consumer, :key=, :api_key=
|
|
72
|
+
def_delegator :@consumer, :secret, :api_key_secret
|
|
73
|
+
def_delegator :@consumer, :secret=, :api_key_secret=
|
|
74
|
+
|
|
75
|
+
DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
|
|
76
|
+
DEFAULT_USER_AGENT = "X-Client/#{X::Version} Ruby/#{RUBY_VERSION}".freeze
|
|
77
|
+
DEFAULT_READ_TIMEOUT = 60 # seconds
|
|
9
78
|
HTTP_METHODS = {
|
|
10
79
|
get: Net::HTTP::Get,
|
|
11
80
|
post: Net::HTTP::Post,
|
|
@@ -13,52 +82,65 @@ module X
|
|
|
13
82
|
delete: Net::HTTP::Delete
|
|
14
83
|
}.freeze
|
|
15
84
|
|
|
16
|
-
def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil
|
|
17
|
-
|
|
85
|
+
def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
|
86
|
+
base_url: DEFAULT_BASE_URL, user_agent: DEFAULT_USER_AGENT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
87
|
+
@base_url = URI(base_url)
|
|
88
|
+
@user_agent = user_agent
|
|
89
|
+
@read_timeout = read_timeout
|
|
18
90
|
|
|
19
|
-
|
|
20
|
-
@bearer_token = bearer_token
|
|
21
|
-
else
|
|
22
|
-
unless api_key && api_key_secret && access_token && access_token_secret
|
|
23
|
-
raise ArgumentError, "Missing OAuth credentials."
|
|
24
|
-
end
|
|
91
|
+
validate_base_url!
|
|
25
92
|
|
|
26
|
-
|
|
27
|
-
|
|
93
|
+
if bearer_token.nil?
|
|
94
|
+
initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
|
95
|
+
else
|
|
96
|
+
@bearer_token = bearer_token
|
|
28
97
|
end
|
|
29
98
|
end
|
|
30
99
|
|
|
31
100
|
def get(endpoint)
|
|
32
|
-
|
|
33
|
-
handle_response(response)
|
|
101
|
+
send_request(:get, endpoint)
|
|
34
102
|
end
|
|
35
103
|
|
|
36
104
|
def post(endpoint, body = nil)
|
|
37
|
-
|
|
38
|
-
handle_response(response)
|
|
105
|
+
send_request(:post, endpoint, body)
|
|
39
106
|
end
|
|
40
107
|
|
|
41
108
|
def put(endpoint, body = nil)
|
|
42
|
-
|
|
43
|
-
handle_response(response)
|
|
109
|
+
send_request(:put, endpoint, body)
|
|
44
110
|
end
|
|
45
111
|
|
|
46
112
|
def delete(endpoint)
|
|
47
|
-
|
|
48
|
-
|
|
113
|
+
send_request(:delete, endpoint)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def base_url=(base_url)
|
|
117
|
+
@base_url = URI(base_url)
|
|
118
|
+
validate_base_url!
|
|
49
119
|
end
|
|
50
120
|
|
|
51
121
|
private
|
|
52
122
|
|
|
123
|
+
def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
|
124
|
+
unless api_key && api_key_secret && access_token && access_token_secret
|
|
125
|
+
raise ArgumentError, "Missing OAuth credentials"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
|
|
129
|
+
@access_token = OAuth::Token.new(access_token, access_token_secret)
|
|
130
|
+
end
|
|
131
|
+
|
|
53
132
|
def send_request(http_method, endpoint, body = nil)
|
|
54
|
-
url = URI.
|
|
133
|
+
url = URI.join(@base_url, endpoint)
|
|
55
134
|
http = Net::HTTP.new(url.host, url.port)
|
|
56
|
-
http.use_ssl =
|
|
135
|
+
http.use_ssl = url.scheme == "https"
|
|
136
|
+
http.read_timeout = @read_timeout
|
|
57
137
|
|
|
58
138
|
request = create_request(http_method, url, body)
|
|
59
|
-
|
|
139
|
+
add_headers(request)
|
|
60
140
|
|
|
61
|
-
http.request(request)
|
|
141
|
+
handle_response(http.request(request))
|
|
142
|
+
rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
|
|
143
|
+
raise X::NetworkError, "Network error: #{e.message}"
|
|
62
144
|
end
|
|
63
145
|
|
|
64
146
|
def create_request(http_method, url, body)
|
|
@@ -71,24 +153,62 @@ module X
|
|
|
71
153
|
request
|
|
72
154
|
end
|
|
73
155
|
|
|
156
|
+
def add_headers(request)
|
|
157
|
+
add_authorization(request)
|
|
158
|
+
add_content_type(request)
|
|
159
|
+
add_user_agent(request)
|
|
160
|
+
end
|
|
161
|
+
|
|
74
162
|
def add_authorization(request)
|
|
75
|
-
if @
|
|
76
|
-
request["Authorization"] = "Bearer #{@bearer_token}"
|
|
77
|
-
else
|
|
163
|
+
if @bearer_token.nil?
|
|
78
164
|
@consumer.sign!(request, @access_token)
|
|
165
|
+
else
|
|
166
|
+
request["Authorization"] = "Bearer #{@bearer_token}"
|
|
79
167
|
end
|
|
80
168
|
end
|
|
81
169
|
|
|
170
|
+
def add_content_type(request)
|
|
171
|
+
request["Content-Type"] = JSON_CONTENT_TYPE
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def add_user_agent(request)
|
|
175
|
+
request["User-Agent"] = @user_agent if @user_agent
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_base_url!
|
|
179
|
+
raise ArgumentError, "Invalid base URL" unless @base_url.is_a?(URI::HTTPS) || @base_url.is_a?(URI::HTTP)
|
|
180
|
+
end
|
|
181
|
+
|
|
82
182
|
def handle_response(response)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
183
|
+
ResponseHandler.new(response).handle
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# HTTP client response handler
|
|
187
|
+
class ResponseHandler
|
|
188
|
+
ERROR_CLASSES = {
|
|
189
|
+
Net::HTTPBadRequest => X::BadRequestError,
|
|
190
|
+
Net::HTTPUnauthorized => X::AuthenticationError,
|
|
191
|
+
Net::HTTPForbidden => X::ForbiddenError,
|
|
192
|
+
Net::HTTPNotFound => X::NotFoundError,
|
|
193
|
+
Net::HTTPTooManyRequests => X::TooManyRequestsError,
|
|
194
|
+
Net::HTTPInternalServerError => X::ServerError,
|
|
195
|
+
Net::HTTPServiceUnavailable => X::ServiceUnavailableError
|
|
196
|
+
}.freeze
|
|
197
|
+
|
|
198
|
+
def initialize(response)
|
|
199
|
+
@response = response
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def handle
|
|
203
|
+
if @response.is_a?(Net::HTTPSuccess) && @response["content-type"] == JSON_CONTENT_TYPE
|
|
204
|
+
return JSON.parse(@response.body)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
error_class = ERROR_CLASSES[@response.class] || X::Error
|
|
208
|
+
error_message = "#{@response.code} #{@response.message}"
|
|
209
|
+
raise error_class, error_message if @response.body.nil? || @response.body.empty?
|
|
210
|
+
|
|
211
|
+
raise error_class.new(@response), error_message
|
|
92
212
|
end
|
|
93
213
|
end
|
|
94
214
|
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
|
+
3
|
|
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.3.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-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: oauth
|
|
@@ -39,7 +39,6 @@ files:
|
|
|
39
39
|
- Rakefile
|
|
40
40
|
- lib/x.rb
|
|
41
41
|
- lib/x/client.rb
|
|
42
|
-
- lib/x/error.rb
|
|
43
42
|
- lib/x/version.rb
|
|
44
43
|
- sig/x.rbs
|
|
45
44
|
homepage: https://github.com/sferik/x-ruby
|