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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +32 -8
- data/lib/threads_client_ruby/version.rb +1 -1
- data/lib/threads_client_ruby.rb +135 -25
- data/logo.jpg +0 -0
- metadata +2 -2
- data/.rubocop.yml +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e8060fb1b838946566a4d2cdab8abbf121811cb52e4b6794c474e2e4adcb28d
|
4
|
+
data.tar.gz: 5b2159ab1d749c87f99c878b5306ee731ab907af1045217db72baf284d307b94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 297041296573f466dd33003b9e750946b6c980779860bdf4db22a9821a3fd51966a238fdf5a475f34ef2b72a797cfe900bf68861ef8952846d79df3059329591
|
7
|
+
data.tar.gz: 4d5e56871d0795bbffb0917f58a82f07c2255406a60083c18bd625e1f47c541b321092ce3b7f3bd74592ee370e6f28b6728dfb78d146f9e8302a1f1a50bd7d54
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# [<img src="logo.jpg" width="36" height="36" />](https://github.com/
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
- [
|
66
|
-
- [
|
67
|
-
- [
|
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
|
-
|
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
|
data/lib/threads_client_ruby.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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] || '
|
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
|
81
|
-
|
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.
|
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