x 0.16.0 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc174269cbe283b3f360fee2c9a443272d0c1d3aab08cf609d552f22323dabc7
4
- data.tar.gz: a2f50383c52f0e499f8afe275ce100cd9ca6f8231bd5fdd1f18c1bfdd7bf5191
3
+ metadata.gz: 4df136d018c47058c7524faada6292a1464b34f2f0e98f2a76e8491630b28d1c
4
+ data.tar.gz: f7101280a194f40bef57a82e9670cd6ddf409e80b7563072384b8f8c7687cb63
5
5
  SHA512:
6
- metadata.gz: d9d9d70a98d38037f5a9606ec4770bc1fb79b7201a4c6f0a3d6a8a7e9360d360730d42606059fe40365ef008bab229cafe14feef5c3558518528afe2c15f6676
7
- data.tar.gz: bceff542ee42df84cc8b96df31e311db88bafb7ecec138c2caf7c07d2c106c535750c1cdc603d22e1743586798f79f9d624c9073dee282093f89dc305e886286
6
+ metadata.gz: 24599b2cb2604a711a1a810cb21b0631ce26519dcee977e12bf494fdde1be55c08bf4e2716b6c281918010847e8c1bd72cd236a44f0343ece70a66f4a908ab1f
7
+ data.tar.gz: 84cdf631377ed38f0400a0f82d6dc42609839eeb4ba81d77526017af8541365152adb4f3e73f2722ae2645a494cf697976c1ae85039c9bd2b82edb1efe97136f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.17.0] - 2025-12-02
2
+ * Add MediaUploader.upload_binary method (9f2f108)
3
+ * Don't forward filename during media upload (492214d)
4
+
1
5
  ## [0.16.0] - 2025-06-24
2
6
  * Remove media_type parameter from non-chunked upload and append methods (f1f38b5)
3
7
  * Fix media upload (dcb418a)
data/README.md CHANGED
@@ -23,7 +23,8 @@ Or, if Bundler is not being used to manage dependencies:
23
23
 
24
24
  ## Usage
25
25
 
26
- First, obtain X credentials from <https://developer.x.com>.
26
+ > [!NOTE]
27
+ > First, obtain X credentials from <https://developer.x.com>.
27
28
 
28
29
  ```ruby
29
30
  require "x"
data/lib/x/connection.rb CHANGED
@@ -13,7 +13,7 @@ module X
13
13
  DEFAULT_OPEN_TIMEOUT = 60 # seconds
14
14
  DEFAULT_READ_TIMEOUT = 60 # seconds
15
15
  DEFAULT_WRITE_TIMEOUT = 60 # seconds
16
- DEFAULT_DEBUG_OUTPUT = File.open(File::NULL, "w")
16
+ DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
17
17
  NETWORK_ERRORS = [
18
18
  Errno::ECONNREFUSED,
19
19
  Errno::ECONNRESET,
@@ -0,0 +1,17 @@
1
+ module X
2
+ module MediaUploadValidator
3
+ module_function
4
+
5
+ MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
6
+
7
+ def validate_file_path!(file_path:)
8
+ raise "File not found: #{file_path}" unless File.exist?(file_path)
9
+ end
10
+
11
+ def validate_media_category!(media_category:)
12
+ return if MEDIA_CATEGORIES.include?(media_category.downcase)
13
+
14
+ raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
15
+ end
16
+ end
17
+ end
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require "securerandom"
3
3
  require "tmpdir"
4
+ require_relative "media_upload_validator"
4
5
 
5
6
  module X
6
7
  module MediaUploader
@@ -8,8 +9,7 @@ module X
8
9
 
9
10
  MAX_RETRIES = 3
10
11
  BYTES_PER_MB = 1_048_576
11
- MEDIA_CATEGORIES = %w[dm_gif dm_image dm_video subtitles tweet_gif tweet_image tweet_video].freeze
12
- DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MEDIA_CATEGORIES
12
+ DM_GIF, DM_IMAGE, DM_VIDEO, SUBTITLES, TWEET_GIF, TWEET_IMAGE, TWEET_VIDEO = MediaUploadValidator::MEDIA_CATEGORIES
13
13
  DEFAULT_MIME_TYPE = "application/octet-stream".freeze
14
14
  MIME_TYPES = %w[image/gif image/jpeg video/mp4 image/png application/x-subrip image/webp].freeze
15
15
  GIF_MIME_TYPE, JPEG_MIME_TYPE, MP4_MIME_TYPE, PNG_MIME_TYPE, SUBRIP_MIME_TYPE, WEBP_MIME_TYPE = MIME_TYPES
@@ -18,15 +18,21 @@ module X
18
18
  PROCESSING_INFO_STATES = %w[failed succeeded].freeze
19
19
 
20
20
  def upload(client:, file_path:, media_category:, boundary: SecureRandom.hex)
21
- validate!(file_path:, media_category:)
22
- upload_body = construct_upload_body(file_path:, media_category:, boundary:)
21
+ MediaUploadValidator.validate_file_path!(file_path:)
22
+ upload_binary(client:, content: File.binread(file_path), media_category:, boundary:)
23
+ end
24
+
25
+ def upload_binary(client:, content:, media_category:, boundary: SecureRandom.hex)
26
+ MediaUploadValidator.validate_media_category!(media_category:)
27
+ upload_body = construct_upload_body(content:, media_category:, boundary:)
23
28
  headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
24
29
  client.post("media/upload", upload_body, headers:)&.fetch("data")
25
30
  end
26
31
 
27
32
  def chunked_upload(client:, file_path:, media_category:, media_type: infer_media_type(file_path, media_category),
28
33
  boundary: SecureRandom.hex, chunk_size_mb: 1)
29
- validate!(file_path:, media_category:)
34
+ MediaUploadValidator.validate_file_path!(file_path:)
35
+ MediaUploadValidator.validate_media_category!(media_category:)
30
36
  media = init(client:, file_path:, media_type:, media_category:)
31
37
  chunk_size = chunk_size_mb * BYTES_PER_MB
32
38
  append(client:, file_paths: split(file_path, chunk_size), media:, boundary:)
@@ -51,14 +57,6 @@ module X
51
57
 
52
58
  private
53
59
 
54
- def validate!(file_path:, media_category:)
55
- raise "File not found: #{file_path}" unless File.exist?(file_path)
56
-
57
- return if MEDIA_CATEGORIES.include?(media_category.downcase)
58
-
59
- raise ArgumentError, "Invalid media_category: #{media_category}. Valid values: #{MEDIA_CATEGORIES.join(", ")}"
60
- end
61
-
62
60
  def infer_media_type(file_path, media_category)
63
61
  case media_category.downcase
64
62
  when TWEET_GIF, DM_GIF then GIF_MIME_TYPE
@@ -87,7 +85,7 @@ module X
87
85
  def append(client:, file_paths:, media:, boundary: SecureRandom.hex)
88
86
  threads = file_paths.map.with_index do |file_path, index|
89
87
  Thread.new do
90
- upload_body = construct_upload_body(file_path:, segment_index: index, boundary:)
88
+ upload_body = construct_upload_body(content: File.binread(file_path), segment_index: index, boundary:)
91
89
  headers = {"Content-Type" => "multipart/form-data; boundary=#{boundary}"}
92
90
  upload_chunk(client:, media_id: media["id"], upload_body:, file_path:, headers:)
93
91
  end
@@ -110,14 +108,14 @@ module X
110
108
  Dir.delete(dirname) if Dir.empty?(dirname)
111
109
  end
112
110
 
113
- def construct_upload_body(file_path:, media_category: nil, segment_index: nil, boundary: SecureRandom.hex)
111
+ def construct_upload_body(content:, media_category: nil, segment_index: nil, boundary: SecureRandom.hex)
114
112
  body = ""
115
113
  body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"segment_index\"\r\n\r\n#{segment_index}\r\n" if segment_index
116
114
  body += "--#{boundary}\r\nContent-Disposition: form-data; name=\"media_category\"\r\n\r\n#{media_category}\r\n" if media_category
117
115
  "#{body}--#{boundary}\r\n" \
118
- "Content-Disposition: form-data; name=\"media\"; filename=\"#{File.basename(file_path)}\"\r\n" \
116
+ "Content-Disposition: form-data; name=\"media\"\r\n" \
119
117
  "Content-Type: #{DEFAULT_MIME_TYPE}\r\n\r\n" \
120
- "#{File.binread(file_path)}\r\n" \
118
+ "#{content}\r\n" \
121
119
  "--#{boundary}--\r\n"
122
120
  end
123
121
  end
@@ -1,5 +1,5 @@
1
1
  require "base64"
2
- require "cgi"
2
+ require "cgi/escape"
3
3
  require "json"
4
4
  require "openssl"
5
5
  require "securerandom"
@@ -38,14 +38,13 @@ module X
38
38
 
39
39
  def add_authentication(request:, authenticator:)
40
40
  authenticator.header(request).each do |key, value|
41
- request.add_field(key, value)
41
+ request[key] = value
42
42
  end
43
43
  end
44
44
 
45
45
  def add_headers(request:, headers:)
46
46
  DEFAULT_HEADERS.merge(headers).each do |key, value|
47
- request.delete(key)
48
- request.add_field(key, value)
47
+ request[key] = value
49
48
  end
50
49
  end
51
50
 
data/lib/x/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.16.0")
4
+ VERSION = Gem::Version.create("0.17.0")
5
5
  end
data/sig/x.rbs CHANGED
@@ -245,7 +245,6 @@ module X
245
245
  module MediaUploader
246
246
  MAX_RETRIES: Integer
247
247
  BYTES_PER_MB: Integer
248
- MEDIA_CATEGORIES: Array[String]
249
248
  DM_GIF: String
250
249
  DM_IMAGE: String
251
250
  DM_VIDEO: String
@@ -266,12 +265,12 @@ module X
266
265
  extend MediaUploader
267
266
 
268
267
  def upload: (client: Client, file_path: String, media_category: String, ?boundary: String) -> untyped
268
+ def upload_binary: (client: Client, content: String, media_category: String, ?boundary: String) -> untyped
269
269
  def chunked_upload: (client: Client, file_path: String, media_category: String, ?media_type: String, ?boundary: String, ?chunk_size_mb: Integer) -> untyped
270
270
  def await_processing: (client: Client, media: untyped) -> untyped
271
271
  def await_processing!: (client: Client, media: untyped) -> untyped
272
272
 
273
273
  private
274
- def validate!: (file_path: String, media_category: String) -> nil
275
274
  def infer_media_type: (String file_path, String media_category) -> String
276
275
  def split: (String file_path, Integer chunk_size) -> Array[String]
277
276
  def init: (client: Client, file_path: String, media_type: String, media_category: String) -> untyped
@@ -279,6 +278,14 @@ module X
279
278
  def upload_chunk: (client: Client, media_id: String, upload_body: String, file_path: String, ?headers: Hash[String, String]) -> Integer?
280
279
  def cleanup_file: (String file_path) -> Integer?
281
280
  def finalize: (client: Client, media: untyped) -> untyped
282
- def construct_upload_body: (file_path: String, ?media_category: String, ?segment_index: Integer, ?boundary: String) -> String
281
+ def construct_upload_body: (content: String, ?media_category: String, ?segment_index: Integer, ?boundary: String) -> String
282
+ end
283
+
284
+ module MediaUploadValidator
285
+ MEDIA_CATEGORIES: Array[String]
286
+ extend MediaUploadValidator
287
+
288
+ def validate_file_path!: (file_path: String) -> nil
289
+ def validate_media_category!: (media_category: String) -> nil
283
290
  end
284
291
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -59,6 +59,7 @@ files:
59
59
  - lib/x/errors/too_many_requests.rb
60
60
  - lib/x/errors/unauthorized.rb
61
61
  - lib/x/errors/unprocessable_entity.rb
62
+ - lib/x/media_upload_validator.rb
62
63
  - lib/x/media_uploader.rb
63
64
  - lib/x/oauth_authenticator.rb
64
65
  - lib/x/rate_limit.rb