x 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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.
|