threads_client_ruby 0.1.0 → 0.1.1

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: 4d5de23a59914609da01d579e6b077f2d025a5ec8ebbfd4b0335efdecf756ffd
4
- data.tar.gz: 9eb0c361f1f7e297e03093c873f5d71d3a7f71cbd90e2a343fe32fe54cea2145
3
+ metadata.gz: 1e8060fb1b838946566a4d2cdab8abbf121811cb52e4b6794c474e2e4adcb28d
4
+ data.tar.gz: 5b2159ab1d749c87f99c878b5306ee731ab907af1045217db72baf284d307b94
5
5
  SHA512:
6
- metadata.gz: 45682ac968778cd53e713fc4ff5a458570447b90e2541c45989d9fa1b8588b444596afbfae39da33f7e579593e8d6506de453657dc7a87c5b9e13675cb1f11ba
7
- data.tar.gz: b095030c2b96238e5e27b5d3836eb63357484425a54c623b72a910438f28271b848406810df6413a09ba56be017941a6efac104da7f37b937c9d5525f78df2d8
6
+ metadata.gz: 297041296573f466dd33003b9e750946b6c980779860bdf4db22a9821a3fd51966a238fdf5a475f34ef2b72a797cfe900bf68861ef8952846d79df3059329591
7
+ data.tar.gz: 4d5e56871d0795bbffb0917f58a82f07c2255406a60083c18bd625e1f47c541b321092ce3b7f3bd74592ee370e6f28b6728dfb78d146f9e8302a1f1a50bd7d54
data/CHANGELOG.md CHANGED
@@ -3,3 +3,11 @@
3
3
  ## [0.1.0] - 2023-07-12
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.1.1] - 2023-07-12
8
+
9
+ - Threads with Image
10
+ - Threads with Link Attachment
11
+ - Get Post Id From URL
12
+ - Reply to Other Threads
13
+ - Fixed major issues
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [<img src="logo.jpg" width="36" height="36" />](https://github.com/junhoyeo) Threads Client
1
+ # [<img src="logo.jpg" width="36" height="36" />](https://github.com/dereknguyen269) Threads Client Ruby
2
2
 
3
3
  > Unofficial, Reverse-Engineered Ruby client for Meta's [Threads](https://threads.net).
4
4
 
@@ -30,7 +30,7 @@ credentials = {
30
30
  password: "Instagram's password"
31
31
  }
32
32
 
33
- ThreadsClient.config do |config|
33
+ ThreadsClientRuby.config do |config|
34
34
  config.credentials = credentials
35
35
  end
36
36
  ```
@@ -42,7 +42,7 @@ credentials = {
42
42
  userid: "Instagram's user id"
43
43
  }
44
44
 
45
- ThreadsClient.config do |config|
45
+ ThreadsClientRuby.config do |config|
46
46
  config.credentials = credentials
47
47
  end
48
48
  ```
@@ -52,7 +52,7 @@ end
52
52
  #### 🤖 Get usertoken and userid
53
53
 
54
54
  ```ruby
55
- userinfo = ThreadsClient.get_userinfo
55
+ userinfo = ThreadsClientRuby.get_userinfo
56
56
  # {
57
57
  # :usertoken=> "eyJkc191c2VyX2lkIjoiNTgzOTIyMTY....",
58
58
  # :userid=>"583922..."
@@ -62,9 +62,10 @@ end
62
62
  #### 📌 Features
63
63
 
64
64
  - [x] ✅ [Text Threads](#✨-threads-with-image)
65
- - [ ] ✅ [Threads with Image](#✨-threads-with-image)
66
- - [ ] ✅ [Threads with Link Attachment](#✨-threads-with-link-attachment)
67
- - [ ] ✅ [Reply to Other Threads](#✨-reply-to-other-threads)
65
+ - [x] ✅ [Threads with Image](#✨-threads-with-image)
66
+ - [x] ✅ [Threads with Link Attachment](#✨-threads-with-link-attachment)
67
+ - [x] ✅ [Get Post Id From URL](#✨-get-post-id-from-url)
68
+ - [x] ✅ [Reply to Other Threads](#✨-reply-to-other-threads)
68
69
  - [ ] ✅ [Like/Unlike a Thread](#✨-likeunlike-a-thread)
69
70
  - [ ] ✅ [Follow/Unfollow a User](#✨-followunfollow-a-user)
70
71
  - [ ] ✅ [Delete a Post](#✨-delete-a-post)
@@ -72,15 +73,38 @@ end
72
73
  **✨ Text Threads**
73
74
 
74
75
  ```ruby
75
- ThreadsClient.publish(text: 'Hello World!')
76
+ ThreadsClientRuby.publish(text: 'Hello World!')
76
77
  ```
77
78
 
78
79
  ###### ✨ Threads with Image
79
80
 
81
+ ```ruby
82
+ # Online image path
83
+ ThreadsClientRuby.publish(text: 'Hello World!', image: 'https://fastly.picsum.photos/id/654/536/354.jpg?hmac=Nqd_oi3EIiPJBAVPYhIUjaEvKpRqLjhtTHkxPmjjo7M')
84
+
85
+ # Or with local image path
86
+ ThreadsClientRuby.publish(text: 'Hello World!', image: '/Users/local-path/logo.jpg')
87
+ ```
88
+
80
89
  ###### ✨ Threads with Link Attachment
81
90
 
91
+ ```ruby
92
+ ThreadsClientRuby.publish(url: 'https://github.com/dereknguyen269/threads_client_ruby')
93
+ ```
94
+
95
+ ###### ✨ Get Post Id From URL
96
+
97
+ ```ruby
98
+ post_id = ThreadsClientRuby.get_post_id('https://www.threads.net/t/CugF-EjhQ3r')
99
+ # => 3143538795635609067
100
+ ```
101
+
82
102
  ###### ✨ Reply to Other Threads
83
103
 
104
+ ```ruby
105
+ ThreadsClientRuby.publish(reply_id: post_id, text: "Reply to #{post_id}")
106
+ ```
107
+
84
108
  ###### ✨ Like/Unlike a Thread
85
109
 
86
110
  ###### ✨ Follow/Unfollow a User
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThreadsClientRuby
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "threads_client_ruby/version"
4
+ require 'securerandom'
4
5
  require 'net/http'
5
6
  require 'uri'
6
7
  require 'json'
8
+ require 'httparty'
9
+ require 'mime/types'
7
10
 
8
11
  module ThreadsClientRuby
9
12
  DEFAULT_DEVICE_ID = "android-#{rand(36**24).to_s(36)}"
@@ -11,6 +14,7 @@ module ThreadsClientRuby
11
14
  BASE_API_URL = 'https://i.instagram.com/api/v1'
12
15
  LOGIN_URL = BASE_API_URL + '/bloks/apps/com.bloks.www.bloks.caa.login.async.send_login_request/'
13
16
  POST_URL = BASE_API_URL + '/media/configure_text_only_post/'
17
+ POST_WITH_IMAGE_URL = BASE_API_URL + '/media/configure_text_post_app_feed/'
14
18
  DEFAULT_LSD_TOKEN = 'NjppQDEgONsU_1LCzrmp6q'
15
19
  class Error < StandardError; end
16
20
  module Config
@@ -36,53 +40,88 @@ module ThreadsClientRuby
36
40
  core.user_info
37
41
  end
38
42
 
43
+ # available key for options:
44
+ # - text
45
+ # - image
46
+ # - url
47
+ # - reply_id
39
48
  def self.publish(options = {})
40
49
  core = ThreadsClientRuby::Core.new ThreadsClientRuby::Config.credentials
41
- if options[:text] && options[:image]
42
- elsif options[:text]
43
- core.publish(options)
44
- elsif options[:image]
45
- else
46
- raise Error.new "Don't have text or image"
47
- end
50
+ core.publish(options)
48
51
  end
49
52
 
53
+ def self.get_post_id_from_url(post_url, options = {})
54
+ uri = URI.parse(post_url)
55
+ response = Net::HTTP.get_response(uri)
56
+ text = response.body.to_s.gsub(/\s+/, '').gsub(/\n+/, '')
57
+
58
+ post_id = text.match(/{"post_id":"(.*?)"}/)&.captures&.first
59
+ lsd_token = text.match(/"LSD",\[\],{"token":"(\w+)"},\d+\]/)&.captures&.first
60
+ post_id
61
+ end
62
+
50
63
  class Core
51
- def initialize(credentials)
52
- @username = credentials[:username]
53
- @password = credentials[:password]
54
- @user_token = credentials[:usertoken]
55
- @user_id = credentials[:userid]
64
+ def initialize(credentials = {})
65
+ if credentials.is_a?(Hash)
66
+ @username = credentials[:username]
67
+ @password = credentials[:password]
68
+ @user_token = credentials[:usertoken]
69
+ @user_id = credentials[:userid]
70
+ else
71
+ raise "Invalid credentials"
72
+ end
56
73
  end
57
74
 
58
75
  def publish(options)
76
+ req_post_url = ThreadsClientRuby::POST_URL
77
+ data = default_req_params(options)
78
+ if options[:image]
79
+ req_post_url = ThreadsClientRuby::POST_WITH_IMAGE_URL
80
+ data = req_params_with_image(data, options[:image])
81
+ else
82
+ data[:publish_mode] = 'text_post'
83
+ end
84
+ if options[:url] || options[:reply_id]
85
+ data[:text_post_app_info] = {}
86
+ data[:text_post_app_info][:link_attachment_url] = options[:url] if options[:url]
87
+ data[:text_post_app_info][:reply_id] = options[:reply_id] if options[:reply_id]
88
+ end
89
+ url = URI.parse(req_post_url)
90
+ headers = get_app_headers
91
+ payload = "signed_body=SIGNATURE.#{URI.encode_www_form_component(JSON.generate(data))}"
92
+ response = HTTParty.post(url, headers: headers, body: payload)
93
+ p response_handler(response)
94
+ end
95
+
96
+ def user_info
97
+ { usertoken: user_token, userid: user_id }
98
+ end
99
+
100
+ private
101
+
102
+ def default_req_params(options)
59
103
  now = Time.now
60
104
  timezone_offset = -now.utc_offset
61
-
62
- data = {
105
+ {
63
106
  text_post_app_info: { reply_control: 0 },
64
107
  timezone_offset: timezone_offset.to_s,
65
108
  source_type: '4',
66
109
  _uid: user_id,
67
110
  device_id: DEFAULT_DEVICE_ID,
68
- caption: options[:text] || 'Please enter the text',
111
+ caption: options[:text] || '',
69
112
  upload_id: now.to_i,
70
113
  device: androidDevice
71
114
  }
72
- data[:publish_mode] = 'text_post'
73
- url = URI.parse(ThreadsClientRuby::POST_URL)
74
- headers = get_app_headers
75
- payload = "signed_body=SIGNATURE.#{URI.encode_www_form_component(JSON.generate(data))}"
76
- response = HTTParty.post(url, headers: headers, body: payload)
77
- p response_handler(response)
78
115
  end
79
116
 
80
- def user_info
81
- { usertoken: user_token, userid: user_id }
117
+ def req_params_with_image(data, image)
118
+ upload_id = Time.now.to_i.to_s
119
+ upload_image(image, upload_id)
120
+ data[:upload_id] = upload_id
121
+ data[:scene_capture_type] = ''
122
+ data
82
123
  end
83
124
 
84
- private
85
-
86
125
  def androidDevice
87
126
  {
88
127
  manufacturer: 'OnePlus',
@@ -189,5 +228,76 @@ module ThreadsClientRuby
189
228
  response_body = JSON.parse(response.body)
190
229
  { status: true, response_body: response_body }
191
230
  end
231
+
232
+ def upload_image(image, upload_id)
233
+ name = "#{upload_id}_0_#{SecureRandom.random_number(10**10 - 10**9 + 1) + 10**9}"
234
+ url = "https://www.instagram.com/rupload_igphoto/#{name}"
235
+
236
+ content = nil
237
+ mime_type = nil
238
+
239
+ if image.is_a?(String) || image.key?(:path)
240
+ image_path = image.is_a?(String) ? image : image[:path]
241
+ is_file_path = !image_path.start_with?('http')
242
+
243
+ if is_file_path
244
+ content = File.binread(image_path)
245
+ mime_type = MIME::Types.type_for(image_path).first.to_s
246
+ else
247
+ image_uri = URI.parse(image_path)
248
+ response = Net::HTTP.get_response(image_uri)
249
+ if response.is_a?(Net::HTTPSuccess)
250
+ content = response.body
251
+ mime_type = response['content-type']
252
+ end
253
+ end
254
+ else
255
+ content = image[:data]
256
+ mime_type = image[:type].include?('/') ? image[:type] : MIME::Types.type_for(image[:type]).first.to_s
257
+ end
258
+
259
+ x_instagram_rupload_params = {
260
+ upload_id: upload_id,
261
+ media_type: '1',
262
+ sticker_burnin_params: JSON.generate([]),
263
+ image_compression: JSON.generate({ lib_name: 'moz', lib_version: '3.1.m', quality: '80' }),
264
+ xsharing_user_ids: JSON.generate([]),
265
+ retry_context: JSON.generate({
266
+ num_step_auto_retry: '0',
267
+ num_reupload: '0',
268
+ num_step_manual_retry: '0',
269
+ }),
270
+ 'IG-FB-Xpost-entry-point-v2': 'feed',
271
+ }
272
+
273
+ content_length = content.length
274
+ image_headers = get_default_headers(@username).merge({
275
+ 'Content-Type': 'application/octet-stream',
276
+ 'X_FB_PHOTO_WATERFALL_ID': SecureRandom.uuid,
277
+ 'X-Entity-Type': mime_type ? "image/#{mime_type}" : 'image/jpeg',
278
+ 'Offset': '0',
279
+ 'X-Instagram-Rupload-Params': JSON.generate(x_instagram_rupload_params),
280
+ 'X-Entity-Name': name,
281
+ 'X-Entity-Length': content_length.to_s,
282
+ 'Content-Length': content_length.to_s,
283
+ 'Accept-Encoding': 'gzip',
284
+ })
285
+
286
+ begin
287
+ response = Net::HTTP.start(URI(url).hostname, URI(url).port, use_ssl: true) do |http|
288
+ request = Net::HTTP::Post.new(URI(url).request_uri, image_headers)
289
+ request.body = content
290
+ http.request(request)
291
+ end
292
+
293
+ data = JSON.parse(response.body)
294
+ data
295
+ rescue StandardError => e
296
+ puts "[UPLOAD_IMAGE] FAILED: #{e.response.body}"
297
+ raise e
298
+ end
299
+ end
192
300
  end
193
301
  end
302
+
303
+ # ThreadsClientRuby.publish(text: 'Hello World!', image: '/Users/quan/Products/threads-api/logo.jpg')
data/logo.jpg ADDED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: threads_client_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Nguyen
@@ -32,7 +32,6 @@ executables: []
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
- - ".rubocop.yml"
36
35
  - CHANGELOG.md
37
36
  - CODE_OF_CONDUCT.md
38
37
  - Gemfile
@@ -43,6 +42,7 @@ files:
43
42
  - bin/setup
44
43
  - lib/threads_client_ruby.rb
45
44
  - lib/threads_client_ruby/version.rb
45
+ - logo.jpg
46
46
  - sig/threads_client_ruby.rbs
47
47
  homepage: https://github.com/dereknguyen269/threads_client_ruby
48
48
  licenses:
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120