x 0.11.0 → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +16 -10
- data/lib/x/authenticator.rb +3 -2
- data/lib/x/bearer_token_authenticator.rb +1 -2
- data/lib/x/cgi.rb +0 -1
- data/lib/x/client.rb +52 -17
- data/lib/x/connection.rb +0 -1
- data/lib/x/errors/client_error.rb +2 -2
- data/lib/x/errors/error.rb +1 -6
- data/lib/x/errors/http_error.rb +41 -0
- data/lib/x/errors/server_error.rb +2 -2
- data/lib/x/errors/too_many_redirects.rb +2 -2
- data/lib/x/errors/too_many_requests.rb +3 -9
- data/lib/x/media_uploader.rb +26 -24
- data/lib/x/oauth_authenticator.rb +1 -2
- data/lib/x/redirect_handler.rb +5 -7
- data/lib/x/request_builder.rb +0 -1
- data/lib/x/response_parser.rb +10 -34
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +35 -27
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 553e0e0e9a87126211bf62942b02cb2c7c8b6d9380ee8fd4336058038c9735b0
|
4
|
+
data.tar.gz: 34922d1e52a0c04de3c6fe090e854174d78d437c8e14b061a95ca4ab5e1a0438
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e28c53b17bb053c26326d46940eb539c4612d6f4a547b074e974c2b6ddee616bb37af6c2dd2d4d84b05a21aa2946306de074889b90bd411b2a268793e40d72a0
|
7
|
+
data.tar.gz: 79ab655009e07915cf71158bb2976301d1519093c753b6f8fc1d128781b4844e867551c3d430bb0226aa113d3c7342efdd401e08d4f3d920a7437dfa8c432ba6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## [0.12.1] - 2023-11-28
|
2
|
+
* Ensure split chunks are written as binary (c6e257f)
|
3
|
+
* Require tmpdir in X::MediaUploader (9e7c7f1)
|
4
|
+
|
5
|
+
## [0.12.0] - 2023-11-02
|
6
|
+
* Ensure Authenticator is passed to RedirectHandler (fc8557b)
|
7
|
+
* Add AUTHENTICATION_HEADER to X::Authenticator base class (85a2818)
|
8
|
+
* Introduce X::HTTPError (90ae132)
|
9
|
+
* Add `code` attribute to error classes (b003639)
|
10
|
+
|
1
11
|
## [0.11.0] - 2023-10-24
|
2
12
|
|
3
13
|
* Add base Authenticator class (8c66ce2)
|
data/README.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
![Tests](https://github.com/sferik/x-ruby/actions/workflows/test.yml/badge.svg)
|
2
|
+
![Linter](https://github.com/sferik/x-ruby/actions/workflows/lint.yml/badge.svg)
|
3
|
+
![Mutant](https://github.com/sferik/x-ruby/actions/workflows/mutant.yml/badge.svg)
|
4
|
+
![Typer Checker](https://github.com/sferik/x-ruby/actions/workflows/type_check.yml/badge.svg)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/x.svg)](https://rubygems.org/gems/x)
|
6
|
+
|
1
7
|
# A [Ruby](https://www.ruby-lang.org) interface to the [X API](https://developer.x.com)
|
2
8
|
|
3
9
|
## Follow
|
@@ -35,24 +41,24 @@ x_client = X::Client.new(**x_credentials)
|
|
35
41
|
x_client.get("users/me")
|
36
42
|
# {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
|
37
43
|
|
38
|
-
# Post
|
39
|
-
|
44
|
+
# Post
|
45
|
+
post = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
|
40
46
|
# {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
|
41
47
|
|
42
|
-
# Delete the
|
43
|
-
x_client.delete("tweets/#{
|
48
|
+
# Delete the post
|
49
|
+
x_client.delete("tweets/#{post["data"]["id"]}")
|
44
50
|
# {"data"=>{"deleted"=>true}}
|
45
51
|
|
46
52
|
# Initialize an API v1.1 client
|
47
53
|
v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_credentials)
|
48
54
|
|
49
|
-
#
|
55
|
+
# Get your account settings
|
50
56
|
v1_client.get("account/settings.json")
|
51
57
|
|
52
58
|
# Initialize an X Ads API client
|
53
59
|
ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_credentials)
|
54
60
|
|
55
|
-
#
|
61
|
+
# Get your ad accounts
|
56
62
|
ads_client.get("accounts")
|
57
63
|
```
|
58
64
|
|
@@ -60,7 +66,7 @@ See other common usage [examples](https://github.com/sferik/x-ruby/tree/main/exa
|
|
60
66
|
|
61
67
|
## History and Philosophy
|
62
68
|
|
63
|
-
This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests)
|
69
|
+
This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests), not counting dependencies. This library is about 500 lines of code (plus 1000 test lines) and has no runtime dependencies. That doesn’t mean new features won’t be added over time, but the benefits of more code must be weighed against the benefits of less:
|
64
70
|
|
65
71
|
* Less code is easier to maintain.
|
66
72
|
* Less code means fewer bugs.
|
@@ -70,9 +76,7 @@ In the immortal words of [Ezra Zygmuntowicz](https://github.com/ezmobius) and hi
|
|
70
76
|
|
71
77
|
> No code is faster than no code.
|
72
78
|
|
73
|
-
The
|
74
|
-
|
75
|
-
The tests for the previous version of this library executed 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.
|
79
|
+
The tests for the previous version of this library executed in about 2 seconds. That sounds pretty fast until you see that tests for this library run in one-twentieth 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.
|
76
80
|
|
77
81
|
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—read the code. The code is always right.
|
78
82
|
|
@@ -97,6 +101,8 @@ Many thanks to our sponsors (listed in order of when they sponsored this project
|
|
97
101
|
<a href="https://betterstack.com"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/better_stack.svg" alt="Better Stack" width="200" align="middle"></a>
|
98
102
|
<img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
|
99
103
|
<a href="https://sentry.io"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/sentry.svg" alt="Sentry" width="200" align="middle"></a>
|
104
|
+
<img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
|
105
|
+
<a href="https://ifttt.com"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/ifttt.svg" alt="IFTTT" width="200" align="middle"></a>
|
100
106
|
|
101
107
|
## Development
|
102
108
|
|
data/lib/x/authenticator.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require_relative "authenticator"
|
2
2
|
|
3
3
|
module X
|
4
|
-
# Handles bearer token authentication
|
5
4
|
class BearerTokenAuthenticator < Authenticator
|
6
5
|
attr_accessor :bearer_token
|
7
6
|
|
@@ -10,7 +9,7 @@ module X
|
|
10
9
|
end
|
11
10
|
|
12
11
|
def header(_request)
|
13
|
-
{
|
12
|
+
{AUTHENTICATION_HEADER => "Bearer #{bearer_token}"}
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
data/lib/x/cgi.rb
CHANGED
data/lib/x/client.rb
CHANGED
@@ -7,16 +7,14 @@ require_relative "request_builder"
|
|
7
7
|
require_relative "response_parser"
|
8
8
|
|
9
9
|
module X
|
10
|
-
# Main public interface
|
11
10
|
class Client
|
12
11
|
extend Forwardable
|
13
12
|
|
14
13
|
DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
|
15
14
|
|
16
15
|
attr_accessor :base_url
|
16
|
+
attr_reader :api_key, :api_key_secret, :access_token, :access_token_secret, :bearer_token
|
17
17
|
|
18
|
-
def_delegators :@authenticator, :bearer_token, :api_key, :api_key_secret, :access_token, :access_token_secret
|
19
|
-
def_delegators :@authenticator, :bearer_token=, :api_key=, :api_key_secret=, :access_token=, :access_token_secret=
|
20
18
|
def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :proxy_url, :debug_output
|
21
19
|
def_delegators :@connection, :open_timeout=, :read_timeout=, :write_timeout=, :proxy_url=, :debug_output=
|
22
20
|
def_delegators :@redirect_handler, :max_redirects
|
@@ -24,25 +22,27 @@ module X
|
|
24
22
|
def_delegators :@response_parser, :array_class, :object_class
|
25
23
|
def_delegators :@response_parser, :array_class=, :object_class=
|
26
24
|
|
27
|
-
def initialize(
|
28
|
-
|
25
|
+
def initialize(api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
26
|
+
bearer_token: nil,
|
29
27
|
base_url: DEFAULT_BASE_URL,
|
30
28
|
open_timeout: Connection::DEFAULT_OPEN_TIMEOUT,
|
31
29
|
read_timeout: Connection::DEFAULT_READ_TIMEOUT,
|
32
30
|
write_timeout: Connection::DEFAULT_WRITE_TIMEOUT,
|
31
|
+
debug_output: Connection::DEFAULT_DEBUG_OUTPUT,
|
33
32
|
proxy_url: nil,
|
34
|
-
debug_output: nil,
|
35
33
|
array_class: nil,
|
36
34
|
object_class: nil,
|
37
35
|
max_redirects: RedirectHandler::DEFAULT_MAX_REDIRECTS)
|
38
36
|
|
37
|
+
initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
38
|
+
@bearer_token = bearer_token
|
39
|
+
initialize_authenticator
|
39
40
|
@base_url = base_url
|
40
|
-
initialize_authenticator(bearer_token, api_key, api_key_secret, access_token, access_token_secret)
|
41
41
|
@connection = Connection.new(open_timeout: open_timeout, read_timeout: read_timeout,
|
42
42
|
write_timeout: write_timeout, debug_output: debug_output, proxy_url: proxy_url)
|
43
43
|
@request_builder = RequestBuilder.new
|
44
|
-
@redirect_handler = RedirectHandler.new(
|
45
|
-
|
44
|
+
@redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder,
|
45
|
+
max_redirects: max_redirects)
|
46
46
|
@response_parser = ResponseParser.new(array_class: array_class, object_class: object_class)
|
47
47
|
end
|
48
48
|
|
@@ -62,25 +62,60 @@ module X
|
|
62
62
|
execute_request(:delete, endpoint, headers: headers)
|
63
63
|
end
|
64
64
|
|
65
|
+
def api_key=(api_key)
|
66
|
+
@api_key = api_key
|
67
|
+
initialize_authenticator
|
68
|
+
end
|
69
|
+
|
70
|
+
def api_key_secret=(api_key_secret)
|
71
|
+
@api_key_secret = api_key_secret
|
72
|
+
initialize_authenticator
|
73
|
+
end
|
74
|
+
|
75
|
+
def access_token=(access_token)
|
76
|
+
@access_token = access_token
|
77
|
+
initialize_authenticator
|
78
|
+
end
|
79
|
+
|
80
|
+
def access_token_secret=(access_token_secret)
|
81
|
+
@access_token_secret = access_token_secret
|
82
|
+
initialize_authenticator
|
83
|
+
end
|
84
|
+
|
85
|
+
def bearer_token=(bearer_token)
|
86
|
+
@bearer_token = bearer_token
|
87
|
+
initialize_authenticator
|
88
|
+
end
|
89
|
+
|
65
90
|
private
|
66
91
|
|
67
|
-
def
|
68
|
-
@
|
69
|
-
|
70
|
-
|
92
|
+
def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
|
93
|
+
@api_key = api_key
|
94
|
+
@api_key_secret = api_key_secret
|
95
|
+
@access_token = access_token
|
96
|
+
@access_token_secret = access_token_secret
|
97
|
+
end
|
98
|
+
|
99
|
+
def initialize_authenticator
|
100
|
+
@authenticator = if api_key && api_key_secret && access_token && access_token_secret
|
71
101
|
OAuthAuthenticator.new(api_key: api_key, api_key_secret: api_key_secret, access_token: access_token,
|
72
102
|
access_token_secret: access_token_secret)
|
73
|
-
|
103
|
+
elsif bearer_token
|
104
|
+
BearerTokenAuthenticator.new(bearer_token: bearer_token)
|
105
|
+
elsif @authenticator.nil?
|
74
106
|
Authenticator.new
|
107
|
+
else
|
108
|
+
@authenticator
|
75
109
|
end
|
76
110
|
end
|
77
111
|
|
78
112
|
def execute_request(http_method, endpoint, headers:, body: nil)
|
79
113
|
uri = URI.join(base_url, endpoint)
|
80
|
-
request = @request_builder.build(
|
81
|
-
|
114
|
+
request = @request_builder.build(http_method: http_method, uri: uri, body: body, headers: headers,
|
115
|
+
authenticator: @authenticator)
|
82
116
|
response = @connection.perform(request: request)
|
83
|
-
response = @redirect_handler.handle(response: response, request: request, base_url: base_url
|
117
|
+
response = @redirect_handler.handle(response: response, request: request, base_url: base_url,
|
118
|
+
authenticator: @authenticator)
|
84
119
|
@response_parser.parse(response: response)
|
85
120
|
end
|
86
121
|
end
|
data/lib/x/connection.rb
CHANGED
data/lib/x/errors/error.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative "error"
|
3
|
+
|
4
|
+
module X
|
5
|
+
class HTTPError < Error
|
6
|
+
JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
|
7
|
+
|
8
|
+
attr_reader :response, :code
|
9
|
+
|
10
|
+
def initialize(response:)
|
11
|
+
super(error_message(response))
|
12
|
+
@response = response
|
13
|
+
@code = response.code
|
14
|
+
end
|
15
|
+
|
16
|
+
def error_message(response)
|
17
|
+
if json?(response)
|
18
|
+
message_from_json_response(response)
|
19
|
+
else
|
20
|
+
response.message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def message_from_json_response(response)
|
25
|
+
response_object = JSON.parse(response.body)
|
26
|
+
if response_object.key?("title") && response_object.key?("detail")
|
27
|
+
"#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
|
28
|
+
elsif response_object.key?("error")
|
29
|
+
response_object.fetch("error")
|
30
|
+
elsif response_object["errors"].instance_of?(Array)
|
31
|
+
response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
|
32
|
+
else
|
33
|
+
response.message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def json?(response)
|
38
|
+
JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,23 +1,17 @@
|
|
1
1
|
require_relative "client_error"
|
2
2
|
|
3
3
|
module X
|
4
|
-
# Rate limit error
|
5
4
|
class TooManyRequests < ClientError
|
6
|
-
def initialize(msg, response)
|
7
|
-
@response = response
|
8
|
-
super(msg)
|
9
|
-
end
|
10
|
-
|
11
5
|
def limit
|
12
|
-
|
6
|
+
response["x-rate-limit-limit"].to_i
|
13
7
|
end
|
14
8
|
|
15
9
|
def remaining
|
16
|
-
|
10
|
+
response["x-rate-limit-remaining"].to_i
|
17
11
|
end
|
18
12
|
|
19
13
|
def reset_at
|
20
|
-
Time.at(
|
14
|
+
Time.at(response["x-rate-limit-reset"].to_i)
|
21
15
|
end
|
22
16
|
|
23
17
|
def reset_in
|
data/lib/x/media_uploader.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "securerandom"
|
2
|
+
require "tmpdir"
|
2
3
|
|
3
4
|
module X
|
4
|
-
# Helper module for uploading images and videos
|
5
5
|
module MediaUploader
|
6
6
|
extend self
|
7
7
|
|
@@ -19,7 +19,7 @@ module X
|
|
19
19
|
boundary: SecureRandom.hex)
|
20
20
|
validate!(file_path: file_path, media_category: media_category)
|
21
21
|
upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
|
22
|
-
upload_body = construct_upload_body(file_path, media_type, boundary)
|
22
|
+
upload_body = construct_upload_body(file_path: file_path, media_type: media_type, boundary: boundary)
|
23
23
|
headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
|
24
24
|
upload_client.post("media/upload.json?media_category=#{media_category}", upload_body, headers: headers)
|
25
25
|
end
|
@@ -28,10 +28,11 @@ module X
|
|
28
28
|
media_category), boundary: SecureRandom.hex, chunk_size_mb: 8)
|
29
29
|
validate!(file_path: file_path, media_category: media_category)
|
30
30
|
upload_client = client.dup.tap { |c| c.base_url = "https://upload.twitter.com/1.1/" }
|
31
|
-
media = init(upload_client, file_path, media_type,
|
31
|
+
media = init(upload_client: upload_client, file_path: file_path, media_type: media_type,
|
32
|
+
media_category: media_category)
|
32
33
|
chunk_size = chunk_size_mb * BYTES_PER_MB
|
33
|
-
|
34
|
-
|
34
|
+
append(upload_client: upload_client, file_paths: split(file_path, chunk_size), media: media,
|
35
|
+
media_type: media_type, boundary: boundary)
|
35
36
|
upload_client.post("media/upload.json?command=FINALIZE&media_id=#{media["media_id"]}")
|
36
37
|
end
|
37
38
|
|
@@ -64,54 +65,55 @@ module X
|
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
|
-
def init(upload_client, file_path, media_type, media_category)
|
68
|
-
total_bytes = File.size(file_path)
|
69
|
-
query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
|
70
|
-
upload_client.post("media/upload.json?#{query}")
|
71
|
-
end
|
72
|
-
|
73
68
|
def split(file_path, chunk_size)
|
74
69
|
file_number = -1
|
75
70
|
|
76
|
-
[].tap do |
|
71
|
+
[].tap do |file_paths|
|
77
72
|
File.open(file_path, "rb") do |f|
|
78
73
|
while (chunk = f.read(chunk_size))
|
79
|
-
|
80
|
-
File.
|
74
|
+
file_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
|
75
|
+
File.binwrite(path, chunk)
|
81
76
|
end
|
82
77
|
end
|
83
78
|
end
|
84
79
|
end
|
85
80
|
end
|
86
81
|
|
87
|
-
def
|
88
|
-
|
82
|
+
def init(upload_client:, file_path:, media_type:, media_category:)
|
83
|
+
total_bytes = File.size(file_path)
|
84
|
+
query = "command=INIT&media_type=#{media_type}&media_category=#{media_category}&total_bytes=#{total_bytes}"
|
85
|
+
upload_client.post("media/upload.json?#{query}")
|
86
|
+
end
|
87
|
+
|
88
|
+
def append(upload_client:, file_paths:, media:, media_type:, boundary: SecureRandom.hex)
|
89
|
+
threads = file_paths.map.with_index do |file_path, index|
|
89
90
|
Thread.new do
|
90
|
-
upload_body = construct_upload_body(
|
91
|
+
upload_body = construct_upload_body(file_path: file_path, media_type: media_type, boundary: boundary)
|
91
92
|
query = "command=APPEND&media_id=#{media["media_id"]}&segment_index=#{index}"
|
92
93
|
headers = {"Content-Type" => "multipart/form-data, boundary=#{boundary}"}
|
93
|
-
upload_chunk(upload_client, query, upload_body,
|
94
|
+
upload_chunk(upload_client: upload_client, query: query, upload_body: upload_body, file_path: file_path,
|
95
|
+
headers: headers)
|
94
96
|
end
|
95
97
|
end
|
96
98
|
threads.each(&:join)
|
97
99
|
end
|
98
100
|
|
99
|
-
def upload_chunk(upload_client
|
101
|
+
def upload_chunk(upload_client:, query:, upload_body:, file_path:, headers: {})
|
100
102
|
upload_client.post("media/upload.json?#{query}", upload_body, headers: headers)
|
101
103
|
rescue NetworkError, ServerError
|
102
104
|
retries ||= 0
|
103
105
|
((retries += 1) < MAX_RETRIES) ? retry : raise
|
104
106
|
ensure
|
105
|
-
|
107
|
+
cleanup_file(file_path)
|
106
108
|
end
|
107
109
|
|
108
|
-
def
|
109
|
-
dirname = File.dirname(
|
110
|
-
File.delete(
|
110
|
+
def cleanup_file(file_path)
|
111
|
+
dirname = File.dirname(file_path)
|
112
|
+
File.delete(file_path)
|
111
113
|
Dir.delete(dirname) if Dir.empty?(dirname)
|
112
114
|
end
|
113
115
|
|
114
|
-
def construct_upload_body(file_path
|
116
|
+
def construct_upload_body(file_path:, media_type:, boundary: SecureRandom.hex)
|
115
117
|
"--#{boundary}\r\n" \
|
116
118
|
"Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
|
117
119
|
"Content-Type: #{media_type}\r\n\r\n" \
|
@@ -7,7 +7,6 @@ require_relative "authenticator"
|
|
7
7
|
require_relative "cgi"
|
8
8
|
|
9
9
|
module X
|
10
|
-
# Handles OAuth authentication
|
11
10
|
class OAuthAuthenticator < Authenticator
|
12
11
|
OAUTH_VERSION = "1.0".freeze
|
13
12
|
OAUTH_SIGNATURE_METHOD = "HMAC-SHA1".freeze
|
@@ -24,7 +23,7 @@ module X
|
|
24
23
|
|
25
24
|
def header(request)
|
26
25
|
method, url, query_params = parse_request(request)
|
27
|
-
{
|
26
|
+
{AUTHENTICATION_HEADER => build_oauth_header(method, url, query_params)}
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
data/lib/x/redirect_handler.rb
CHANGED
@@ -6,28 +6,26 @@ require_relative "errors/too_many_redirects"
|
|
6
6
|
require_relative "request_builder"
|
7
7
|
|
8
8
|
module X
|
9
|
-
# Handles HTTP redirects
|
10
9
|
class RedirectHandler
|
11
10
|
DEFAULT_MAX_REDIRECTS = 10
|
12
11
|
|
13
12
|
attr_accessor :max_redirects
|
14
|
-
attr_reader :
|
13
|
+
attr_reader :connection, :request_builder
|
15
14
|
|
16
|
-
def initialize(
|
15
|
+
def initialize(connection: Connection.new, request_builder: RequestBuilder.new,
|
17
16
|
max_redirects: DEFAULT_MAX_REDIRECTS)
|
18
|
-
@authenticator = authenticator
|
19
17
|
@connection = connection
|
20
18
|
@request_builder = request_builder
|
21
19
|
@max_redirects = max_redirects
|
22
20
|
end
|
23
21
|
|
24
|
-
def handle(response:, request:, base_url:, redirect_count: 0)
|
22
|
+
def handle(response:, request:, base_url:, authenticator: Authenticator.new, redirect_count: 0)
|
25
23
|
if response.is_a?(Net::HTTPRedirection)
|
26
24
|
raise TooManyRedirects, "Too many redirects" if redirect_count > max_redirects
|
27
25
|
|
28
26
|
new_uri = build_new_uri(response, base_url)
|
29
27
|
|
30
|
-
new_request = build_request(request, new_uri, Integer(response.code))
|
28
|
+
new_request = build_request(request, new_uri, Integer(response.code), authenticator)
|
31
29
|
new_response = connection.perform(request: new_request)
|
32
30
|
|
33
31
|
handle(response: new_response, request: new_request, base_url: base_url, redirect_count: redirect_count + 1)
|
@@ -44,7 +42,7 @@ module X
|
|
44
42
|
URI.join(base_url, location)
|
45
43
|
end
|
46
44
|
|
47
|
-
def build_request(request, new_uri, response_code)
|
45
|
+
def build_request(request, new_uri, response_code, authenticator)
|
48
46
|
http_method, body = case response_code
|
49
47
|
in 307 | 308
|
50
48
|
[request.method.downcase.to_sym, request.body]
|
data/lib/x/request_builder.rb
CHANGED
data/lib/x/response_parser.rb
CHANGED
@@ -3,7 +3,7 @@ require "net/http"
|
|
3
3
|
require_relative "errors/bad_gateway"
|
4
4
|
require_relative "errors/bad_request"
|
5
5
|
require_relative "errors/connection_exception"
|
6
|
-
require_relative "errors/
|
6
|
+
require_relative "errors/http_error"
|
7
7
|
require_relative "errors/forbidden"
|
8
8
|
require_relative "errors/gateway_timeout"
|
9
9
|
require_relative "errors/gone"
|
@@ -17,9 +17,8 @@ require_relative "errors/unauthorized"
|
|
17
17
|
require_relative "errors/unprocessable_entity"
|
18
18
|
|
19
19
|
module X
|
20
|
-
# Process HTTP responses
|
21
20
|
class ResponseParser
|
22
|
-
|
21
|
+
ERROR_MAP = {
|
23
22
|
400 => BadRequest,
|
24
23
|
401 => Unauthorized,
|
25
24
|
403 => Forbidden,
|
@@ -35,7 +34,7 @@ module X
|
|
35
34
|
503 => ServiceUnavailable,
|
36
35
|
504 => GatewayTimeout
|
37
36
|
}.freeze
|
38
|
-
JSON_CONTENT_TYPE_REGEXP = %r{application/
|
37
|
+
JSON_CONTENT_TYPE_REGEXP = %r{application/json}
|
39
38
|
|
40
39
|
attr_accessor :array_class, :object_class
|
41
40
|
|
@@ -45,48 +44,25 @@ module X
|
|
45
44
|
end
|
46
45
|
|
47
46
|
def parse(response:)
|
48
|
-
raise error(response) unless
|
47
|
+
raise error(response) unless response.is_a?(Net::HTTPSuccess)
|
49
48
|
|
50
|
-
|
49
|
+
return unless json?(response)
|
50
|
+
|
51
|
+
JSON.parse(response.body, array_class: array_class, object_class: object_class)
|
51
52
|
end
|
52
53
|
|
53
54
|
private
|
54
55
|
|
55
|
-
def success?(response)
|
56
|
-
response.is_a?(Net::HTTPSuccess)
|
57
|
-
end
|
58
|
-
|
59
56
|
def error(response)
|
60
|
-
error_class(response).new(
|
57
|
+
error_class(response).new(response: response)
|
61
58
|
end
|
62
59
|
|
63
60
|
def error_class(response)
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
def error_message(response)
|
68
|
-
if json?(response)
|
69
|
-
message_from_json_response(response)
|
70
|
-
else
|
71
|
-
response.message
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def message_from_json_response(response)
|
76
|
-
response_object = JSON.parse(response.body)
|
77
|
-
if response_object.key?("title") && response_object.key?("detail")
|
78
|
-
"#{response_object.fetch("title")}: #{response_object.fetch("detail")}"
|
79
|
-
elsif response_object.key?("error")
|
80
|
-
response_object.fetch("error")
|
81
|
-
elsif response_object["errors"].instance_of?(Array)
|
82
|
-
response_object.fetch("errors").map { |error| error.fetch("message") }.join(", ")
|
83
|
-
else
|
84
|
-
response.message
|
85
|
-
end
|
61
|
+
ERROR_MAP[Integer(response.code)] || HTTPError
|
86
62
|
end
|
87
63
|
|
88
64
|
def json?(response)
|
89
|
-
|
65
|
+
JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
|
90
66
|
end
|
91
67
|
end
|
92
68
|
end
|
data/lib/x/version.rb
CHANGED
data/sig/x.rbs
CHANGED
@@ -2,16 +2,18 @@ module X
|
|
2
2
|
VERSION: Gem::Version
|
3
3
|
|
4
4
|
class Authenticator
|
5
|
+
AUTHENTICATION_HEADER: String
|
6
|
+
|
5
7
|
def header: (Net::HTTPRequest? request) -> Hash[String, String]
|
6
8
|
end
|
7
9
|
|
8
|
-
class BearerTokenAuthenticator
|
10
|
+
class BearerTokenAuthenticator < Authenticator
|
9
11
|
attr_accessor bearer_token: String
|
10
12
|
def initialize: (bearer_token: String) -> void
|
11
13
|
def header: (Net::HTTPRequest? request) -> Hash[String, String]
|
12
14
|
end
|
13
15
|
|
14
|
-
class OAuthAuthenticator
|
16
|
+
class OAuthAuthenticator < Authenticator
|
15
17
|
OAUTH_VERSION: String
|
16
18
|
OAUTH_SIGNATURE_METHOD: String
|
17
19
|
OAUTH_SIGNATURE_ALGORITHM: String
|
@@ -37,7 +39,10 @@ module X
|
|
37
39
|
def escape: (String value) -> String
|
38
40
|
end
|
39
41
|
|
40
|
-
class
|
42
|
+
class Error < StandardError
|
43
|
+
end
|
44
|
+
|
45
|
+
class ClientError < HTTPError
|
41
46
|
end
|
42
47
|
|
43
48
|
class BadGateway < ClientError
|
@@ -49,8 +54,18 @@ module X
|
|
49
54
|
class ConnectionException < ClientError
|
50
55
|
end
|
51
56
|
|
52
|
-
class
|
53
|
-
|
57
|
+
class HTTPError < Error
|
58
|
+
JSON_CONTENT_TYPE_REGEXP: Regexp
|
59
|
+
|
60
|
+
attr_reader response : Net::HTTPResponse
|
61
|
+
attr_reader code : String
|
62
|
+
|
63
|
+
def initialize: (response: Net::HTTPResponse) -> void
|
64
|
+
|
65
|
+
private
|
66
|
+
def error_message: (Net::HTTPResponse response) -> String
|
67
|
+
def message_from_json_response: (Net::HTTPResponse response) -> String
|
68
|
+
def json?: (Net::HTTPResponse response) -> bool
|
54
69
|
end
|
55
70
|
|
56
71
|
class Forbidden < ClientError
|
@@ -77,19 +92,18 @@ module X
|
|
77
92
|
class PayloadTooLarge < ClientError
|
78
93
|
end
|
79
94
|
|
80
|
-
class ServerError <
|
95
|
+
class ServerError < HTTPError
|
81
96
|
end
|
82
97
|
|
83
98
|
class ServiceUnavailable < ServerError
|
84
99
|
end
|
85
100
|
|
86
|
-
class TooManyRedirects <
|
101
|
+
class TooManyRedirects < Error
|
87
102
|
end
|
88
103
|
|
89
104
|
class TooManyRequests < ClientError
|
90
105
|
@response: Net::HTTPResponse
|
91
106
|
|
92
|
-
def initialize: (String msg, Net::HTTPResponse response) -> void
|
93
107
|
def limit: -> Integer
|
94
108
|
def remaining: -> Integer
|
95
109
|
def reset_at: -> Time
|
@@ -154,19 +168,19 @@ module X
|
|
154
168
|
attr_reader connection: Connection
|
155
169
|
attr_reader request_builder: RequestBuilder
|
156
170
|
attr_reader max_redirects: Integer
|
157
|
-
def initialize: (
|
158
|
-
def handle: (response: Net::HTTPResponse, request: Net::HTTPRequest, base_url: String, ?redirect_count: Integer) -> Net::HTTPResponse
|
171
|
+
def initialize: (connection: Connection, request_builder: RequestBuilder, ?max_redirects: Integer) -> void
|
172
|
+
def handle: (response: Net::HTTPResponse, request: Net::HTTPRequest, base_url: String, ?authenticator: Authenticator, ?redirect_count: Integer) -> Net::HTTPResponse
|
159
173
|
|
160
174
|
private
|
161
175
|
def build_new_uri: (Net::HTTPResponse response, String base_url) -> URI::Generic
|
162
|
-
def build_request: (Net::HTTPRequest request, URI::Generic new_uri, Integer response_code) -> Net::HTTPRequest
|
176
|
+
def build_request: (Net::HTTPRequest request, URI::Generic new_uri, Integer response_code, Authenticator authenticator) -> Net::HTTPRequest
|
163
177
|
def send_new_request: (URI::Generic new_uri, Net::HTTPRequest new_request) -> Net::HTTPResponse
|
164
178
|
end
|
165
179
|
|
166
180
|
class ResponseParser
|
167
181
|
DEFAULT_ARRAY_CLASS: Class
|
168
182
|
DEFAULT_OBJECT_CLASS: Class
|
169
|
-
|
183
|
+
ERROR_MAP: Hash[Integer, singleton(Unauthorized) | singleton(BadRequest) | singleton(Forbidden) | singleton(InternalServerError) | singleton(NotFound) | singleton(PayloadTooLarge) | singleton(ServiceUnavailable) | singleton(TooManyRequests)]
|
170
184
|
JSON_CONTENT_TYPE_REGEXP: Regexp
|
171
185
|
|
172
186
|
attr_accessor array_class: Class
|
@@ -175,11 +189,8 @@ module X
|
|
175
189
|
def parse: (response: Net::HTTPResponse) -> untyped
|
176
190
|
|
177
191
|
private
|
178
|
-
def success?: (Net::HTTPResponse response) -> bool
|
179
192
|
def error: (Net::HTTPResponse response) -> (Unauthorized | BadRequest | Forbidden | InternalServerError | NotFound | PayloadTooLarge | ServiceUnavailable | TooManyRequests)
|
180
193
|
def error_class: (Net::HTTPResponse response) -> (singleton(Unauthorized) | singleton(BadRequest) | singleton(Forbidden) | singleton(InternalServerError) | singleton(NotFound) | singleton(PayloadTooLarge) | singleton(ServiceUnavailable) | singleton(TooManyRequests))
|
181
|
-
def error_message: (Net::HTTPResponse response) -> String
|
182
|
-
def message_from_json_response: (Net::HTTPResponse response) -> String
|
183
194
|
def json?: (Net::HTTPResponse response) -> bool
|
184
195
|
end
|
185
196
|
|
@@ -213,14 +224,15 @@ module X
|
|
213
224
|
attr_accessor redirect_handler: RedirectHandler
|
214
225
|
attr_accessor response_parser: ResponseParser
|
215
226
|
|
216
|
-
def initialize: (?
|
227
|
+
def initialize: (?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?bearer_token: String?, ?base_url: String, ?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO, ?array_class: Class, ?object_class: Class, ?max_redirects: Integer) -> void
|
217
228
|
def get: (String endpoint, ?headers: Hash[String, String]) -> untyped
|
218
229
|
def post: (String endpoint, ?String? body, ?headers: Hash[String, String]) -> untyped
|
219
230
|
def put: (String endpoint, ?String? body, ?headers: Hash[String, String]) -> untyped
|
220
231
|
def delete: (String endpoint, ?headers: Hash[String, String]) -> untyped
|
221
232
|
|
222
233
|
private
|
223
|
-
def
|
234
|
+
def initialize_oauth: (String? api_key, String? api_key_secret, String? access_token, String? access_token_secret) -> void
|
235
|
+
def initialize_authenticator: -> Authenticator
|
224
236
|
def execute_request: (Symbol http_method, String endpoint, headers: Hash[String, String], ?body: String?) -> untyped
|
225
237
|
end
|
226
238
|
|
@@ -253,13 +265,13 @@ module X
|
|
253
265
|
private
|
254
266
|
def validate!: (file_path: String, media_category: String) -> nil
|
255
267
|
def infer_media_type: (String file_path, String media_category) -> String
|
256
|
-
def init: (Client upload_client, String file_path, String media_type, String media_category) -> untyped
|
257
268
|
def split: (String file_path, Integer chunk_size) -> Array[String]
|
258
|
-
def
|
259
|
-
def
|
260
|
-
def
|
261
|
-
def
|
262
|
-
def
|
269
|
+
def init: (upload_client: Client, file_path: String, media_type: String, media_category: String) -> untyped
|
270
|
+
def append: (upload_client: Client, file_paths: Array[String], media: untyped, media_type: String, ?boundary: String) -> Array[String]
|
271
|
+
def upload_chunk: (upload_client: Client, query: String, upload_body: String, file_path: String, ?headers: Hash[String, String]) -> Integer?
|
272
|
+
def cleanup_file: (String file_path) -> Integer?
|
273
|
+
def finalize: (upload_client: Client, media: untyped) -> untyped
|
274
|
+
def construct_upload_body: (file_path: String, media_type: String, ?boundary: String) -> String
|
263
275
|
end
|
264
276
|
|
265
277
|
class CGI
|
@@ -267,7 +279,3 @@ module X
|
|
267
279
|
def self.escape_params: (Hash[String, String] | Array[[String, String]] params) -> String
|
268
280
|
end
|
269
281
|
end
|
270
|
-
|
271
|
-
class Dir
|
272
|
-
def self.mktmpdir: (?String? prefix_suffix) -> String
|
273
|
-
end
|
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.12.1
|
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-
|
11
|
+
date: 2023-11-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- lib/x/errors/forbidden.rb
|
37
37
|
- lib/x/errors/gateway_timeout.rb
|
38
38
|
- lib/x/errors/gone.rb
|
39
|
+
- lib/x/errors/http_error.rb
|
39
40
|
- lib/x/errors/internal_server_error.rb
|
40
41
|
- lib/x/errors/network_error.rb
|
41
42
|
- lib/x/errors/not_acceptable.rb
|
@@ -80,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
81
|
- !ruby/object:Gem::Version
|
81
82
|
version: '0'
|
82
83
|
requirements: []
|
83
|
-
rubygems_version: 3.4.
|
84
|
+
rubygems_version: 3.4.22
|
84
85
|
signing_key:
|
85
86
|
specification_version: 4
|
86
87
|
summary: A Ruby interface to the X API.
|