x 0.9.0 → 0.10.0
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 -1
- data/README.md +8 -0
- data/lib/x/client.rb +3 -0
- data/lib/x/connection.rb +21 -7
- data/lib/x/errors/error.rb +1 -4
- data/lib/x/errors/internal_server_error.rb +5 -0
- data/lib/x/errors/network_error.rb +2 -2
- data/lib/x/errors/payload_too_large_error.rb +5 -0
- data/lib/x/media_upload.rb +125 -0
- data/lib/x/oauth_authenticator.rb +4 -8
- data/lib/x/redirect_handler.rb +1 -3
- data/lib/x/request_builder.rb +17 -5
- data/lib/x/response_handler.rb +24 -11
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +92 -16
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a01e8a21951f3b998cfae0253c4d35c5412ac39f94681283b9059d4a196651e
|
4
|
+
data.tar.gz: 5b4a5bb02d86b27391b005acb329893025222c20e9d0c99b88ed0d7e6322dc5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8972eafdb4c041a61258f409fef5a93dc0ff56b70e5c8dd384e126652c1ab32a4e52dc56bd253d7c204ea6313e3ac321e93c7245c722c0d5491b59443c63435a
|
7
|
+
data.tar.gz: 4a5dfb958b61b78cdcf9312250d3ba00182894241da1c3e9114f08f1029fb174be07043f607a864986e3d09d6d308dfac4d13549e2483d5746235b34c4dd05a2
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
## [
|
1
|
+
## [0.10.0] - 2023-10-08
|
2
|
+
|
3
|
+
- Add media upload helper methods (6c6a267)
|
4
|
+
- Add PayloadTooLargeError class (cd61850)
|
5
|
+
|
6
|
+
## [0.9.1] - 2023-10-06
|
7
|
+
|
8
|
+
- Allow successful empty responses (06bf7db)
|
9
|
+
- Update default User-Agent string (296b36a)
|
10
|
+
- Move query parameter escaping into RequestBuilder (56d6bd2)
|
2
11
|
|
3
12
|
## [0.9.0] - 2023-09-26
|
4
13
|
|
data/README.md
CHANGED
@@ -90,6 +90,14 @@ Building and maintaining an open-source project like this takes a considerable a
|
|
90
90
|
|
91
91
|
[Click here to sponsor this project.](https://github.com/sponsors/sferik)
|
92
92
|
|
93
|
+
## Sponsors
|
94
|
+
|
95
|
+
Many thanks to our sponsors (listed in order of when they sponsored this project):
|
96
|
+
|
97
|
+
<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
|
+
<img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
|
99
|
+
<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>
|
100
|
+
|
93
101
|
## Development
|
94
102
|
|
95
103
|
1. Checkout and repo:
|
data/lib/x/client.rb
CHANGED
@@ -19,6 +19,9 @@ module X
|
|
19
19
|
def_delegators :@request_builder, :content_type=, :user_agent=
|
20
20
|
def_delegators :@response_handler, :array_class, :object_class
|
21
21
|
def_delegators :@response_handler, :array_class=, :object_class=
|
22
|
+
alias_method :base_url, :base_uri
|
23
|
+
alias_method :base_url=, :base_uri=
|
24
|
+
attr_accessor :authenticator, :connection, :request_builder, :redirect_handler, :response_handler
|
22
25
|
|
23
26
|
def initialize(bearer_token: nil,
|
24
27
|
api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
data/lib/x/connection.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "forwardable"
|
2
2
|
require "net/http"
|
3
|
+
require "openssl"
|
3
4
|
require "uri"
|
4
5
|
require_relative "errors/network_error"
|
5
6
|
|
@@ -16,8 +17,10 @@ module X
|
|
16
17
|
DEFAULT_WRITE_TIMEOUT = 60 # seconds
|
17
18
|
NETWORK_ERRORS = [
|
18
19
|
Errno::ECONNREFUSED,
|
20
|
+
Errno::ECONNRESET,
|
19
21
|
Net::OpenTimeout,
|
20
|
-
Net::ReadTimeout
|
22
|
+
Net::ReadTimeout,
|
23
|
+
OpenSSL::SSL::SSLError
|
21
24
|
].freeze
|
22
25
|
|
23
26
|
attr_reader :base_uri, :proxy_uri, :http_client
|
@@ -39,9 +42,9 @@ module X
|
|
39
42
|
end
|
40
43
|
|
41
44
|
def send_request(request)
|
42
|
-
@http_client.request(request)
|
45
|
+
response = @http_client.request(request)
|
43
46
|
rescue *NETWORK_ERRORS => e
|
44
|
-
raise NetworkError
|
47
|
+
raise NetworkError.new("Network error: #{e.message}", response: response)
|
45
48
|
end
|
46
49
|
|
47
50
|
def base_uri=(base_url)
|
@@ -56,6 +59,17 @@ module X
|
|
56
59
|
@http_client.instance_variable_get(:@debug_output)
|
57
60
|
end
|
58
61
|
|
62
|
+
def configuration
|
63
|
+
{
|
64
|
+
base_url: base_uri.to_s,
|
65
|
+
open_timeout: open_timeout,
|
66
|
+
read_timeout: read_timeout,
|
67
|
+
write_timeout: write_timeout,
|
68
|
+
proxy_url: proxy_uri.to_s,
|
69
|
+
debug_output: debug_output
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
59
73
|
private
|
60
74
|
|
61
75
|
def apply_http_client_settings(open_timeout:, read_timeout:, write_timeout:, debug_output:)
|
@@ -84,15 +98,15 @@ module X
|
|
84
98
|
end
|
85
99
|
|
86
100
|
def build_http_client(host:, port:)
|
87
|
-
if @proxy_uri
|
88
|
-
Net::HTTP.new(host, port)
|
89
|
-
else
|
101
|
+
if defined?(@proxy_uri)
|
90
102
|
Net::HTTP.new(host, port, @proxy_uri&.host, @proxy_uri&.port, @proxy_uri&.user, @proxy_uri&.password)
|
103
|
+
else
|
104
|
+
Net::HTTP.new(host, port)
|
91
105
|
end
|
92
106
|
end
|
93
107
|
|
94
108
|
def conditionally_apply_http_client_settings
|
95
|
-
if @http_client
|
109
|
+
if defined?(@http_client)
|
96
110
|
settings = current_http_client_settings
|
97
111
|
yield
|
98
112
|
apply_http_client_settings(**settings)
|
data/lib/x/errors/error.rb
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
require "json"
|
2
|
-
require "net/http"
|
3
2
|
|
4
3
|
module X
|
5
4
|
# Base error class
|
6
5
|
class Error < ::StandardError
|
7
|
-
JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
|
8
|
-
|
9
6
|
attr_reader :object
|
10
7
|
|
11
8
|
def initialize(msg, response:)
|
12
|
-
@object = JSON.parse(response.body
|
9
|
+
@object = JSON.parse(response.body) if response&.body && !response.body.empty?
|
13
10
|
super(msg)
|
14
11
|
end
|
15
12
|
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module X
|
4
|
+
# Helper module for uploading images and videos
|
5
|
+
module MediaUpload
|
6
|
+
extend self
|
7
|
+
|
8
|
+
MAX_RETRIES = 3
|
9
|
+
BYTES_PER_MB = 1_048_576
|
10
|
+
MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
|
11
|
+
DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MEDIA_CATEGORIES
|
12
|
+
DEFAULT_MIME_TYPE = "application/octet-stream".freeze
|
13
|
+
MIME_TYPES = %w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
|
14
|
+
GIF_MIME_TYPE, JPEG_MIME_TYPE, MP4_MIME_TYPE, PNG_MIME_TYPE, SUBRIP_MIME_TYPE, WEBP_MIME_TYPE = MIME_TYPES
|
15
|
+
MIME_TYPE_MAP = {"gif" => GIF_MIME_TYPE, "jpg" => JPEG_MIME_TYPE, "jpeg" => JPEG_MIME_TYPE, "mp4" => MP4_MIME_TYPE,
|
16
|
+
"png" => PNG_MIME_TYPE, "srt" => SUBRIP_MIME_TYPE, "webp" => WEBP_MIME_TYPE}.freeze
|
17
|
+
|
18
|
+
def media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
|
19
|
+
boundary: SecureRandom.hex)
|
20
|
+
validate!(file_path: file_path, media_category: media_category)
|
21
|
+
upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
|
22
|
+
upload_body = construct_upload_body(file_path, media_type, boundary)
|
23
|
+
upload_client.content_type = "multipart/form-data, boundary=#{boundary}"
|
24
|
+
upload_client.post("media/upload.json?media_category=#{media_category}", upload_body)
|
25
|
+
end
|
26
|
+
|
27
|
+
def chunked_media_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path,
|
28
|
+
media_category), boundary: SecureRandom.hex, chunk_size_mb: 8)
|
29
|
+
validate!(file_path: file_path, media_category: media_category)
|
30
|
+
upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
|
31
|
+
media = init(upload_client, file_path, media_type, media_category)
|
32
|
+
chunk_size = chunk_size_mb * BYTES_PER_MB
|
33
|
+
chunk_paths = split(file_path, chunk_size)
|
34
|
+
append(upload_client, chunk_paths, media, media_type, boundary)
|
35
|
+
upload_client.post("media/upload.json?command=FINALIZE&media_id=#{media["media_id"]}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def await_processing(client:, media:)
|
39
|
+
upload_client = client.dup.tap { |c| c.base_uri = "https://upload.twitter.com/1.1/" }
|
40
|
+
loop do
|
41
|
+
status = upload_client.get("media/upload.json?command=STATUS&media_id=#{media["media_id"]}")
|
42
|
+
return status if status["processing_info"]["state"] == "succeeded"
|
43
|
+
|
44
|
+
sleep status["processing_info"]["check_after_secs"].to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def validate!(file_path:, media_category:)
|
51
|
+
raise "File not found: #{file_path}" unless File.exist?(file_path)
|
52
|
+
|
53
|
+
return if MEDIA_CATEGORIES.include?(media_category.downcase)
|
54
|
+
|
55
|
+
raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def infer_media_type(file_path, media_category)
|
59
|
+
case media_category.downcase
|
60
|
+
when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
|
61
|
+
when TWEET_VIDEO, DM_VIDEO then MP4_MIME_TYPE
|
62
|
+
when SUBTITLES then SUBRIP_MIME_TYPE
|
63
|
+
else MIME_TYPE_MAP[File.extname(file_path).delete(".").downcase] || DEFAULT_MIME_TYPE
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
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
|
+
def split(file_path, chunk_size)
|
74
|
+
file_number = -1
|
75
|
+
|
76
|
+
[].tap do |chunk_paths|
|
77
|
+
File.open(file_path, "rb") do |f|
|
78
|
+
while (chunk = f.read(chunk_size))
|
79
|
+
chunk_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
|
80
|
+
File.write(path, chunk)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def append(upload_client, chunk_paths, media, media_type, boundary = SecureRandom.hex)
|
88
|
+
threads = chunk_paths.map.with_index do |chunk_path, index|
|
89
|
+
Thread.new do
|
90
|
+
upload_body = construct_upload_body(chunk_path, media_type, boundary)
|
91
|
+
query = "command=APPEND&media_id=#{media["media_id"]}&segment_index=#{index}"
|
92
|
+
upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
threads.each(&:join)
|
96
|
+
end
|
97
|
+
|
98
|
+
def upload_chunk(upload_client, query, upload_body, chunk_path, boundary)
|
99
|
+
# Initialize a new client to avoid shared connection issues
|
100
|
+
client = upload_client.dup
|
101
|
+
client.connection = Connection.new(**upload_client.connection.configuration.merge(base_url: "https://upload.twitter.com/1.1/"))
|
102
|
+
client.content_type = "multipart/form-data, boundary=#{boundary}"
|
103
|
+
client.post("media/upload.json?#{query}", upload_body)
|
104
|
+
rescue NetworkError, ServerError
|
105
|
+
retries ||= 0
|
106
|
+
((retries += 1) < MAX_RETRIES) ? retry : raise
|
107
|
+
ensure
|
108
|
+
cleanup_chunk(chunk_path)
|
109
|
+
end
|
110
|
+
|
111
|
+
def cleanup_chunk(chunk_path)
|
112
|
+
dirname = File.dirname(chunk_path)
|
113
|
+
File.delete(chunk_path)
|
114
|
+
Dir.delete(dirname) if Dir.empty?(dirname)
|
115
|
+
end
|
116
|
+
|
117
|
+
def construct_upload_body(file_path, media_type, boundary = SecureRandom.hex)
|
118
|
+
"--#{boundary}\r\n" \
|
119
|
+
"Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
|
120
|
+
"Content-Type: #{media_type}\r\n\r\n" \
|
121
|
+
"#{File.read(file_path)}\r\n" \
|
122
|
+
"--#{boundary}--\r\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -72,22 +72,18 @@ module X
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def signature_base_string(method, url, params)
|
75
|
-
"#{method}&#{
|
76
|
-
end
|
77
|
-
|
78
|
-
def encode_params(params)
|
79
|
-
params.sort.map { |k, v| "#{k}=#{encode(v.to_s)}" }.join("&")
|
75
|
+
"#{method}&#{escape(url)}&#{escape(URI.encode_www_form(params.sort))}"
|
80
76
|
end
|
81
77
|
|
82
78
|
def signing_key
|
83
|
-
"#{
|
79
|
+
"#{escape(api_key_secret)}&#{escape(access_token_secret)}"
|
84
80
|
end
|
85
81
|
|
86
82
|
def format_oauth_header(params)
|
87
|
-
"OAuth #{params.sort.map { |k, v| "#{k}=\"#{
|
83
|
+
"OAuth #{params.sort.map { |k, v| "#{k}=\"#{escape(v.to_s)}\"" }.join(", ")}"
|
88
84
|
end
|
89
85
|
|
90
|
-
def
|
86
|
+
def escape(value)
|
91
87
|
# TODO: Replace CGI.escape with CGI.escapeURIComponent when support for Ruby 3.1 is dropped
|
92
88
|
CGI.escape(value.to_s).gsub("+", "%20")
|
93
89
|
end
|
data/lib/x/redirect_handler.rb
CHANGED
@@ -47,9 +47,7 @@ module X
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def send_new_request(new_uri, new_request)
|
50
|
-
@connection = Connection.new(base_url: new_uri
|
51
|
-
read_timeout: connection.read_timeout, write_timeout: connection.write_timeout,
|
52
|
-
debug_output: connection.debug_output, proxy_url: connection.proxy_uri)
|
50
|
+
@connection = Connection.new(**connection.configuration.merge(base_url: new_uri))
|
53
51
|
connection.send_request(new_request)
|
54
52
|
end
|
55
53
|
end
|
data/lib/x/request_builder.rb
CHANGED
@@ -12,7 +12,7 @@ module X
|
|
12
12
|
delete: Net::HTTP::Delete
|
13
13
|
}.freeze
|
14
14
|
DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8".freeze
|
15
|
-
DEFAULT_USER_AGENT = "X-Client/#{VERSION}
|
15
|
+
DEFAULT_USER_AGENT = "X-Client/#{VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION} (#{RUBY_PLATFORM})".freeze
|
16
16
|
AUTHORIZATION_HEADER = "Authorization".freeze
|
17
17
|
CONTENT_TYPE_HEADER = "Content-Type".freeze
|
18
18
|
USER_AGENT_HEADER = "User-Agent".freeze
|
@@ -24,22 +24,30 @@ module X
|
|
24
24
|
@user_agent = user_agent
|
25
25
|
end
|
26
26
|
|
27
|
-
def build(authenticator, http_method,
|
28
|
-
request = create_request(http_method,
|
27
|
+
def build(authenticator, http_method, uri, body: nil)
|
28
|
+
request = create_request(http_method, uri, body)
|
29
29
|
add_authorization(request, authenticator)
|
30
30
|
add_content_type(request)
|
31
31
|
add_user_agent(request)
|
32
32
|
request
|
33
33
|
end
|
34
34
|
|
35
|
+
def configuration
|
36
|
+
{
|
37
|
+
content_type: content_type,
|
38
|
+
user_agent: user_agent
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
35
42
|
private
|
36
43
|
|
37
|
-
def create_request(http_method,
|
44
|
+
def create_request(http_method, uri, body)
|
38
45
|
http_method_class = HTTP_METHODS[http_method]
|
39
46
|
|
40
47
|
raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
|
41
48
|
|
42
|
-
|
49
|
+
escaped_uri = escape_query_params(uri)
|
50
|
+
request = http_method_class.new(escaped_uri)
|
43
51
|
request.body = body if body && http_method != :get
|
44
52
|
request
|
45
53
|
end
|
@@ -55,5 +63,9 @@ module X
|
|
55
63
|
def add_user_agent(request)
|
56
64
|
request.add_field(USER_AGENT_HEADER, user_agent) if user_agent
|
57
65
|
end
|
66
|
+
|
67
|
+
def escape_query_params(uri)
|
68
|
+
URI(uri).tap { |u| u.query = URI.encode_www_form(URI.decode_www_form(uri.query)) if uri.query }
|
69
|
+
end
|
58
70
|
end
|
59
71
|
end
|
data/lib/x/response_handler.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require "json"
|
2
2
|
require "net/http"
|
3
|
-
require_relative "errors/bad_request_error"
|
4
3
|
require_relative "errors/authentication_error"
|
4
|
+
require_relative "errors/bad_request_error"
|
5
5
|
require_relative "errors/forbidden_error"
|
6
6
|
require_relative "errors/not_found_error"
|
7
|
-
require_relative "errors/
|
8
|
-
require_relative "errors/
|
7
|
+
require_relative "errors/payload_too_large_error"
|
8
|
+
require_relative "errors/internal_server_error"
|
9
9
|
require_relative "errors/service_unavailable_error"
|
10
|
+
require_relative "errors/too_many_requests_error"
|
10
11
|
|
11
12
|
module X
|
12
13
|
# Process HTTP responses
|
@@ -18,8 +19,9 @@ module X
|
|
18
19
|
401 => AuthenticationError,
|
19
20
|
403 => ForbiddenError,
|
20
21
|
404 => NotFoundError,
|
22
|
+
413 => PayloadTooLargeError,
|
21
23
|
429 => TooManyRequestsError,
|
22
|
-
500 =>
|
24
|
+
500 => InternalServerError,
|
23
25
|
503 => ServiceUnavailableError
|
24
26
|
}.freeze
|
25
27
|
JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
|
@@ -32,19 +34,30 @@ module X
|
|
32
34
|
end
|
33
35
|
|
34
36
|
def handle(response)
|
35
|
-
if
|
36
|
-
|
37
|
+
if success?(response)
|
38
|
+
JSON.parse(response.body, array_class: array_class, object_class: object_class) if json?(response)
|
39
|
+
else
|
40
|
+
error_class = ERROR_CLASSES[response.code.to_i] || Error
|
41
|
+
error_message = "#{response.code} #{response.message}"
|
42
|
+
raise error_class.new(error_message, response: response)
|
37
43
|
end
|
44
|
+
end
|
38
45
|
|
39
|
-
|
40
|
-
|
41
|
-
|
46
|
+
def configuration
|
47
|
+
{
|
48
|
+
array_class: array_class,
|
49
|
+
object_class: object_class
|
50
|
+
}
|
42
51
|
end
|
43
52
|
|
44
53
|
private
|
45
54
|
|
46
|
-
def
|
47
|
-
response.is_a?(Net::HTTPSuccess)
|
55
|
+
def success?(response)
|
56
|
+
response.is_a?(Net::HTTPSuccess)
|
57
|
+
end
|
58
|
+
|
59
|
+
def json?(response)
|
60
|
+
response.body && JSON_CONTENT_TYPE_REGEXP.match?(response["content-type"])
|
48
61
|
end
|
49
62
|
end
|
50
63
|
end
|
data/lib/x/version.rb
CHANGED
data/sig/x.rbs
CHANGED
@@ -28,17 +28,16 @@ module X
|
|
28
28
|
def generate_signature: (String method, String url, Hash[String, String] params) -> String
|
29
29
|
def hmac_signature: (String base_string) -> String
|
30
30
|
def signature_base_string: (String method, String url, Hash[String, String] params) -> String
|
31
|
-
def encode_params: (Hash[String, String] params) -> String
|
32
31
|
def signing_key: -> String
|
33
32
|
def format_oauth_header: (Hash[String, String] params) -> String
|
34
|
-
def
|
33
|
+
def escape: (String value) -> String
|
35
34
|
end
|
36
35
|
|
37
36
|
class Error < StandardError
|
38
37
|
JSON_CONTENT_TYPE_REGEXP: Regexp
|
39
38
|
|
40
|
-
attr_reader object:
|
41
|
-
def initialize: (String msg, response: Net::HTTPResponse) -> void
|
39
|
+
attr_reader object: untyped
|
40
|
+
def initialize: (String msg, response: Net::HTTPResponse?) -> void
|
42
41
|
|
43
42
|
private
|
44
43
|
def json_response?: (Net::HTTPResponse response) -> bool
|
@@ -56,9 +55,15 @@ module X
|
|
56
55
|
class ForbiddenError < ClientError
|
57
56
|
end
|
58
57
|
|
58
|
+
class InternalServerError < ServerError
|
59
|
+
end
|
60
|
+
|
59
61
|
class NotFoundError < ClientError
|
60
62
|
end
|
61
63
|
|
64
|
+
class PayloadTooLargeError < ClientError
|
65
|
+
end
|
66
|
+
|
62
67
|
class TooManyRedirectsError < ClientError
|
63
68
|
end
|
64
69
|
|
@@ -88,7 +93,8 @@ module X
|
|
88
93
|
DEFAULT_OPEN_TIMEOUT: Integer
|
89
94
|
DEFAULT_READ_TIMEOUT: Integer
|
90
95
|
DEFAULT_WRITE_TIMEOUT: Integer
|
91
|
-
NETWORK_ERRORS: Array[(singleton(
|
96
|
+
NETWORK_ERRORS: Array[(singleton(Errno::ECONNREFUSED) | singleton(Errno::ECONNRESET) | singleton(Net::OpenTimeout) | singleton(Net::ReadTimeout) | singleton(OpenSSL::SSL::SSLError))]
|
97
|
+
|
92
98
|
extend Forwardable
|
93
99
|
@http_client: Net::HTTP
|
94
100
|
|
@@ -101,6 +107,7 @@ module X
|
|
101
107
|
def send_request: (Net::HTTPRequest request) -> Net::HTTPResponse
|
102
108
|
def base_uri=: (URI::Generic | String base_url) -> void
|
103
109
|
def debug_output: -> IO?
|
110
|
+
def configuration: -> Hash[Symbol, untyped]
|
104
111
|
|
105
112
|
private
|
106
113
|
def apply_http_client_settings: (open_timeout: Float | Integer, read_timeout: Float | Integer, write_timeout: Float | Integer, debug_output: IO?) -> untyped
|
@@ -121,13 +128,15 @@ module X
|
|
121
128
|
attr_accessor content_type: String
|
122
129
|
attr_accessor user_agent: String
|
123
130
|
def initialize: (?content_type: String, ?user_agent: String) -> void
|
124
|
-
def build: (BearerTokenAuthenticator | OauthAuthenticator authenticator, Symbol http_method, URI::Generic
|
131
|
+
def build: (BearerTokenAuthenticator | OauthAuthenticator authenticator, Symbol http_method, URI::Generic uri, ?body: String?) -> (Net::HTTPRequest)
|
132
|
+
def configuration: -> Hash[Symbol, untyped]
|
125
133
|
|
126
134
|
private
|
127
|
-
def create_request: (Symbol http_method, URI::Generic
|
135
|
+
def create_request: (Symbol http_method, URI::Generic uri, String? body) -> (Net::HTTPRequest)
|
128
136
|
def add_authorization: (Net::HTTPRequest request, BearerTokenAuthenticator | OauthAuthenticator authenticator) -> void
|
129
137
|
def add_content_type: (Net::HTTPRequest request) -> void
|
130
138
|
def add_user_agent: (Net::HTTPRequest request) -> void
|
139
|
+
def escape_query_params: (URI::Generic uri) -> URI::Generic
|
131
140
|
end
|
132
141
|
|
133
142
|
class RedirectHandler
|
@@ -149,16 +158,18 @@ module X
|
|
149
158
|
class ResponseHandler
|
150
159
|
DEFAULT_ARRAY_CLASS: Class
|
151
160
|
DEFAULT_OBJECT_CLASS: Class
|
152
|
-
ERROR_CLASSES: Hash[Integer, singleton(AuthenticationError) | singleton(BadRequestError) | singleton(ForbiddenError) | singleton(NotFoundError) | singleton(
|
161
|
+
ERROR_CLASSES: Hash[Integer, singleton(AuthenticationError) | singleton(BadRequestError) | singleton(ForbiddenError) | singleton(InternalServerError) | singleton(NotFoundError) | singleton(PayloadTooLargeError) | singleton(ServiceUnavailableError) | singleton(TooManyRequestsError)]
|
153
162
|
JSON_CONTENT_TYPE_REGEXP: Regexp
|
154
163
|
|
155
164
|
attr_accessor array_class: Class
|
156
165
|
attr_accessor object_class: Class
|
157
166
|
def initialize: (?array_class: Class, ?object_class: Class) -> void
|
158
|
-
def handle: (Net::HTTPResponse response) ->
|
167
|
+
def handle: (Net::HTTPResponse response) -> untyped
|
168
|
+
def configuration: -> Hash[Symbol, Class]
|
159
169
|
|
160
170
|
private
|
161
|
-
def
|
171
|
+
def success?: (Net::HTTPResponse response) -> bool
|
172
|
+
def json?: (Net::HTTPResponse response) -> bool
|
162
173
|
end
|
163
174
|
|
164
175
|
class Client
|
@@ -169,15 +180,80 @@ module X
|
|
169
180
|
@redirect_handler: RedirectHandler
|
170
181
|
@response_handler: ResponseHandler
|
171
182
|
|
172
|
-
|
183
|
+
attr_accessor access_token: String
|
184
|
+
attr_accessor access_token_secret: String
|
185
|
+
attr_accessor api_key: String
|
186
|
+
attr_accessor api_key_secret: String
|
187
|
+
attr_accessor bearer_token: String
|
188
|
+
attr_accessor base_uri: URI::Generic | String
|
189
|
+
attr_accessor open_timeout: Float | Integer
|
190
|
+
attr_accessor read_timeout: Float | Integer
|
191
|
+
attr_accessor write_timeout: Float | Integer
|
192
|
+
attr_accessor proxy_url: String
|
193
|
+
attr_accessor content_type: String
|
194
|
+
attr_accessor user_agent: String
|
195
|
+
attr_accessor debug_output: IO?
|
196
|
+
attr_accessor array_class: Class
|
197
|
+
attr_accessor object_class: Class
|
198
|
+
attr_accessor max_redirects: Integer
|
199
|
+
attr_accessor authenticator: BearerTokenAuthenticator | OauthAuthenticator
|
200
|
+
attr_accessor connection: Connection
|
201
|
+
attr_accessor request_builder: RequestBuilder
|
202
|
+
attr_accessor redirect_handler: RedirectHandler
|
203
|
+
attr_accessor response_handler: ResponseHandler
|
204
|
+
alias base_url base_uri
|
205
|
+
|
206
|
+
|
173
207
|
def initialize: (?bearer_token: String?, ?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?base_url: URI::Generic | String, ?content_type: String, ?user_agent: 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
|
174
|
-
def get: (String endpoint) ->
|
175
|
-
def post: (String endpoint, ?
|
176
|
-
def put: (String endpoint, ?
|
177
|
-
def delete: (String endpoint) ->
|
208
|
+
def get: (String endpoint) -> untyped
|
209
|
+
def post: (String endpoint, ?String? body) -> untyped
|
210
|
+
def put: (String endpoint, ?String? body) -> untyped
|
211
|
+
def delete: (String endpoint) -> untyped
|
178
212
|
|
179
213
|
private
|
180
214
|
def initialize_authenticator: (String? bearer_token, String? api_key, String? api_key_secret, String? access_token, String? access_token_secret) -> (BearerTokenAuthenticator | OauthAuthenticator)
|
181
|
-
def send_request: (Symbol http_method, String endpoint, ?
|
215
|
+
def send_request: (Symbol http_method, String endpoint, ?String? body) -> untyped
|
182
216
|
end
|
217
|
+
|
218
|
+
module MediaUpload
|
219
|
+
MAX_RETRIES: Integer
|
220
|
+
BYTES_PER_MB: Integer
|
221
|
+
MEDIA_CATEGORIES: Array[String]
|
222
|
+
DM_GIF: String
|
223
|
+
DM_IMAGE: String
|
224
|
+
DM_VIDEO: String
|
225
|
+
SUBTITLES: String
|
226
|
+
TWEET_GIF: String
|
227
|
+
TWEET_IMAGE: String
|
228
|
+
TWEET_VIDEO: String
|
229
|
+
DEFAULT_MIME_TYPE: String
|
230
|
+
MIME_TYPES: Array[String]
|
231
|
+
GIF_MIME_TYPE: String
|
232
|
+
JPEG_MIME_TYPE: String
|
233
|
+
MP4_MIME_TYPE: String
|
234
|
+
PNG_MIME_TYPE: String
|
235
|
+
SUBRIP_MIME_TYPE: String
|
236
|
+
WEBP_MIME_TYPE: String
|
237
|
+
MIME_TYPE_MAP: Hash[String, String]
|
238
|
+
extend MediaUpload
|
239
|
+
|
240
|
+
def media_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String) -> untyped
|
241
|
+
def chunked_media_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String, ?chunk_size_mb: Integer) -> untyped
|
242
|
+
def await_processing: (client: Client, media: untyped) -> untyped
|
243
|
+
|
244
|
+
private
|
245
|
+
def validate!: (file_path: String, media_category: String) -> nil
|
246
|
+
def infer_media_type: (String file_path, String media_category) -> String
|
247
|
+
def init: (Client upload_client, String file_path, String media_type, String media_category) -> untyped
|
248
|
+
def split: (String file_path, Integer chunk_size) -> Array[String]
|
249
|
+
def append: (Client upload_client, Array[String] chunk_paths, untyped media, String media_type, ?String boundary) -> Array[String]
|
250
|
+
def upload_chunk: (Client upload_client, String query, String chunk_path, String media_type, String boundary) -> Integer?
|
251
|
+
def cleanup_chunk: (String chunk_path) -> Integer?
|
252
|
+
def finalize: (Client upload_client, untyped media) -> untyped
|
253
|
+
def construct_upload_body: (String file_path, String media_type, ?String boundary) -> String
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class Dir
|
258
|
+
def self.mktmpdir: (?String? prefix_suffix) -> String
|
183
259
|
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.10.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-
|
11
|
+
date: 2023-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -31,12 +31,15 @@ files:
|
|
31
31
|
- lib/x/errors/client_error.rb
|
32
32
|
- lib/x/errors/error.rb
|
33
33
|
- lib/x/errors/forbidden_error.rb
|
34
|
+
- lib/x/errors/internal_server_error.rb
|
34
35
|
- lib/x/errors/network_error.rb
|
35
36
|
- lib/x/errors/not_found_error.rb
|
37
|
+
- lib/x/errors/payload_too_large_error.rb
|
36
38
|
- lib/x/errors/server_error.rb
|
37
39
|
- lib/x/errors/service_unavailable_error.rb
|
38
40
|
- lib/x/errors/too_many_redirects_error.rb
|
39
41
|
- lib/x/errors/too_many_requests_error.rb
|
42
|
+
- lib/x/media_upload.rb
|
40
43
|
- lib/x/oauth_authenticator.rb
|
41
44
|
- lib/x/redirect_handler.rb
|
42
45
|
- lib/x/request_builder.rb
|
@@ -69,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
72
|
- !ruby/object:Gem::Version
|
70
73
|
version: '0'
|
71
74
|
requirements: []
|
72
|
-
rubygems_version: 3.4.
|
75
|
+
rubygems_version: 3.4.20
|
73
76
|
signing_key:
|
74
77
|
specification_version: 4
|
75
78
|
summary: A Ruby interface to the X API.
|