threads-api 0.0.1.pre → 0.1.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/README.md +110 -1
- data/lib/threads/api/client.rb +108 -0
- data/lib/threads/api/oauth2/client.rb +2 -2
- data/lib/threads/api/thread.rb +82 -0
- data/lib/threads/api/version.rb +1 -1
- data/lib/threads/api.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bc7d9996e4a119f65407f455788ff761710855ceb5e8d01eae7853a0d5c8022
|
4
|
+
data.tar.gz: d13d7280d9f19a53cc3acca6d58bb7871e5f1faa5e93767adfb4e3a2f8a9a83c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a51396bdc11e60e5a853a1a04bbce0cf35ba3b4a1685ce81da34a8e230c1b5f647dd4f27dc2bf0985e835d6ff75db9c68c926c73d46c197feef55096b1555672
|
7
|
+
data.tar.gz: 580e44a8a236e33b4489e55db83b58d04d5a2450639bda7960c24ee55ef9db5fdfdab1f60352af504b215cae499c688cdb80e45c6dfc829b8c83a073ed8df7fa
|
data/README.md
CHANGED
@@ -49,7 +49,116 @@ access_token = response.access_token
|
|
49
49
|
expires_at = Time.now + response.expires_in
|
50
50
|
```
|
51
51
|
|
52
|
-
Once you have a valid access token, whether it's short-lived or long-lived, you can use it to make requests to the Threads API
|
52
|
+
Once you have a valid access token, whether it's short-lived or long-lived, you can use it to make requests to the Threads API using a `Threads::API::Client`:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
client = Threads::API::Client.new(access_token)
|
56
|
+
```
|
57
|
+
|
58
|
+
## Reading Threads
|
59
|
+
|
60
|
+
To read threads for a user:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# List recent threads for a user.
|
64
|
+
response = client.list_threads # Defaults to the authenticated user
|
65
|
+
response = client.list_threads(user_id: "7770386109746442")
|
66
|
+
|
67
|
+
# By default, the Threads API returns 25 threads at a time. You can paginate through them like so:
|
68
|
+
next_page = client.list_threads(after: response.after_cursor) # or
|
69
|
+
previous_page = client.list_threads(before: response.before_cursor)
|
70
|
+
|
71
|
+
# Get a specific thread by ID.
|
72
|
+
thread = client.get_thread("18050206876707110") # Defaults to the authenticated user
|
73
|
+
thread = client.get_thread("18050206876707110", user_id: "7770386109746442")
|
74
|
+
```
|
75
|
+
|
76
|
+
`Threads::API::Client#list_threads` accepts the following options:
|
77
|
+
|
78
|
+
* `user_id` - The ID of the user whose threads you want to read. Defaults to `"me"`, the authenticated user.
|
79
|
+
* `fields` - An Array (or comma-separated String) of fields to include in the response. By default, only `id` is requested. See the [Threads API documentation](https://developers.facebook.com/docs/threads/threads-media#fields) for a list of available fields.
|
80
|
+
* `since` - An ISO 8601 date string. Only threads published after this date will be returned.
|
81
|
+
* `until` - An ISO 8601 date string. Only threads published before this date will be returned.
|
82
|
+
* `before` - A cursor string returned by a previous request for pagination.
|
83
|
+
* `after` - A cursor string returned by a previous request for pagination.
|
84
|
+
* `limit` - The number of threads to return. Defaults to `25`, with a maximum of `100`.
|
85
|
+
|
86
|
+
`Threads::API::Client#get_thread` accepts only the `user_id` and `fields` options.
|
87
|
+
|
88
|
+
## Posting to Threads
|
89
|
+
|
90
|
+
Posting to Threads is, at the very least, a two-step process. Threads requires that you first create a container for the media you want to post, then explicitly publishing that container as a thread. However, more steps are involved if you want to post multiple media items in a single thread.
|
91
|
+
|
92
|
+
### Creating the Thread
|
93
|
+
|
94
|
+
The first step in posting to Threads is to create a "media container", even if your post is text-only.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# Create a text-only post
|
98
|
+
client.create_thread(text: "Hello, world!")
|
99
|
+
|
100
|
+
# Create a post with a photo or video
|
101
|
+
client.create_thread(type: "IMAGE", image_url: "https://example.com/image.jpg", text: "Some optional text")
|
102
|
+
client.create_thread(type: "VIDEO", video_url: "https://example.com/video.mp4", text: "Some optional text")
|
103
|
+
|
104
|
+
# Reply to one of your own threads
|
105
|
+
client.create_thread(text: "Hello, world!", reply_to_id: "18050206876707110")
|
106
|
+
|
107
|
+
# Control who can reply to your thread. Defaults to "everyone".
|
108
|
+
client.create_thread(text: "Hello, world!", reply_control: "accounts_you_follow") # or "mentioned_only"
|
109
|
+
```
|
110
|
+
|
111
|
+
### Publishing the Thread
|
112
|
+
|
113
|
+
Once you've created a media container, you can publish it as a thread:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
pending_thread = client.create_thread(text: "Hello, world!")
|
117
|
+
client.publish_thread(pending_thread.id)
|
118
|
+
```
|
119
|
+
|
120
|
+
According to Meta, you may need to wait before attempting to publish a thread, especially if the thread contains images or videos. They suggest checking the status of the pending thread before attempting to publish it:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
pending_thread = client.create_thread(text: "Hello, world!")
|
124
|
+
pending_thread = client.get_thread_status(pending_thread.id)
|
125
|
+
|
126
|
+
while pending_thread.in_progress?
|
127
|
+
# Wait a bit (they recommend checking only once per minute) and try again
|
128
|
+
sleep 60
|
129
|
+
pending_thread = client.get_thread_status(pending_thread.id)
|
130
|
+
end
|
131
|
+
|
132
|
+
if pending_thread.finished?
|
133
|
+
client.publish_thread(pending_thread.id)
|
134
|
+
elsif pending_thread.errored?
|
135
|
+
# Handle the error
|
136
|
+
else
|
137
|
+
# Unpublished threads expire after 24 hours.
|
138
|
+
pending_thread.expired?
|
139
|
+
|
140
|
+
# If you've already published the thread, the status will be "PUBLISHED".
|
141
|
+
pending_thread.published?
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
### Posting multiple photos and/or videos
|
146
|
+
|
147
|
+
Threads allows you to post a combination of up to 10 photos and/or videos in a single thread. To do so, you must first create a media container for each photo or video you want to post, then create a media container for the thread itself, and finally publish the thread.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# Create carousel items for each photo or video you want to post
|
151
|
+
image1 = client.create_carousel_item(type: "IMAGE", image_url: "https://example.com/image1.jpg")
|
152
|
+
image2 = client.create_carousel_item(type: "IMAGE", image_url: "https://example.com/image2.jpg")
|
153
|
+
video1 = client.create_carousel_item(type: "VIDEO", video_url: "https://example.com/video1.mp4")
|
154
|
+
video2 = client.create_carousel_item(type: "VIDEO", video_url: "https://example.com/video2.mp4")
|
155
|
+
|
156
|
+
# Create the media container for the thread itself
|
157
|
+
pending_thread = client.create_carousel_thread(text: "Some optional text", children: [image1.id, image2.id, video1.id, video2.id])
|
158
|
+
|
159
|
+
# Publish the thread
|
160
|
+
client.publish_thread(pending_thread.id)
|
161
|
+
```
|
53
162
|
|
54
163
|
## Contributing
|
55
164
|
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require_relative "thread"
|
2
|
+
|
3
|
+
module Threads
|
4
|
+
module API
|
5
|
+
class Client
|
6
|
+
def initialize(access_token)
|
7
|
+
@access_token = access_token
|
8
|
+
end
|
9
|
+
|
10
|
+
def list_threads(user_id: "me", **options)
|
11
|
+
params = options.slice(:since, :until, :before, :after, :limit).compact
|
12
|
+
params[:access_token] = @access_token
|
13
|
+
|
14
|
+
fields = Array(options[:fields]).join(",")
|
15
|
+
params[:fields] = fields unless fields.empty?
|
16
|
+
|
17
|
+
response = connection.get("#{user_id}/threads", params)
|
18
|
+
|
19
|
+
Threads::API::Thread::List.new(response.body)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_thread(thread_id, fields: nil)
|
23
|
+
params = {access_token: @access_token}
|
24
|
+
params[:fields] = Array(fields).join(",") if fields
|
25
|
+
|
26
|
+
response = connection.get(thread_id, params)
|
27
|
+
|
28
|
+
Threads::API::Thread.new(response.body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_thread(type: "TEXT", text: nil, image_url: nil, video_url: nil, reply_to_id: nil, reply_control: nil)
|
32
|
+
params = {access_token: @access_token, media_type: type, text: text}
|
33
|
+
params[:reply_to_id] = reply_to_id if reply_to_id
|
34
|
+
params[:reply_control] = reply_control if reply_control
|
35
|
+
|
36
|
+
case type
|
37
|
+
when "IMAGE"
|
38
|
+
params[:image_url] = image_url || raise(ArgumentError, "The `:image_url` option is required when the post's type is \"IMAGE\"")
|
39
|
+
when "VIDEO"
|
40
|
+
params[:video_url] = video_url || raise(ArgumentError, "The `:video_url` option is required when the post's type is \"VIDEO\"")
|
41
|
+
when "TEXT"
|
42
|
+
raise ArgumentError, "The `:text` option is required when the post's type is \"TEXT\"" if text.nil? || text.empty?
|
43
|
+
else
|
44
|
+
raise ArgumentError, "Invalid post type: #{type}. Must be one of: \"TEXT\", \"IMAGE\", or \"VIDEO\""
|
45
|
+
end
|
46
|
+
|
47
|
+
response = connection.post("me/threads", params)
|
48
|
+
|
49
|
+
Threads::API::UnpublishedThread.new(response.body)
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_thread_status(thread_id)
|
53
|
+
response = connection.get(thread_id, {
|
54
|
+
access_token: @access_token,
|
55
|
+
fields: "id,status,error_message"
|
56
|
+
})
|
57
|
+
|
58
|
+
Threads::API::ThreadStatus.new(response.body)
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_carousel_item(type:, image_url: nil, video_url: nil)
|
62
|
+
params = {access_token: @access_token, media_type: type, is_carousel_item: true}
|
63
|
+
|
64
|
+
case type
|
65
|
+
when "IMAGE"
|
66
|
+
params[:image_url] = image_url || raise(ArgumentError, "The `:image_url` option is required when the item's type is \"IMAGE\"")
|
67
|
+
when "VIDEO"
|
68
|
+
params[:video_url] = video_url || raise(ArgumentError, "The `:video_url` option is required when the item's type is \"VIDEO\"")
|
69
|
+
else
|
70
|
+
raise ArgumentError, "Invalid item type: #{type}. Must be \"IMAGE\" or \"VIDEO\""
|
71
|
+
end
|
72
|
+
|
73
|
+
response = connection.post("me/threads", params)
|
74
|
+
|
75
|
+
Threads::API::UnpublishedThread.new(response.body)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_carousel_thread(children:, text: nil)
|
79
|
+
params = {access_token: @access_token, media_type: "CAROUSEL", text: text}
|
80
|
+
params[:children] = Array(children).join(",")
|
81
|
+
|
82
|
+
raise ArgumentError, "At least one item must be present in the `:children` option" if params[:children].empty?
|
83
|
+
|
84
|
+
response = connection.post("me/threads", params)
|
85
|
+
|
86
|
+
Threads::API::UnpublishedThread.new(response.body)
|
87
|
+
end
|
88
|
+
|
89
|
+
def publish_thread(id)
|
90
|
+
response = connection.post("me/threads_publish", {access_token: @access_token, creation_id: id})
|
91
|
+
|
92
|
+
Threads::API::ThreadStatus.new(response.body)
|
93
|
+
end
|
94
|
+
alias_method :publish_carousel, :publish_thread
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def connection
|
99
|
+
@connection ||= Faraday.new(url: "https://graph.threads.net/v1.0/") do |f|
|
100
|
+
f.request :url_encoded
|
101
|
+
|
102
|
+
f.response :json
|
103
|
+
f.response :raise_error
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -2,7 +2,7 @@ module Threads
|
|
2
2
|
module API
|
3
3
|
module OAuth2
|
4
4
|
class Client
|
5
|
-
ShortLivedResponse = Struct.new(:access_token, :user_id)
|
5
|
+
ShortLivedResponse = Struct.new(:access_token, :user_id, :error_type, :error_message, :code)
|
6
6
|
LongLivedResponse = Struct.new(:access_token, :token_type, :expires_in)
|
7
7
|
|
8
8
|
def initialize(client_id:, client_secret:)
|
@@ -19,7 +19,7 @@ module Threads
|
|
19
19
|
redirect_uri: redirect_uri
|
20
20
|
})
|
21
21
|
|
22
|
-
ShortLivedResponse.new(*response.body.values_at("access_token", "user_id"))
|
22
|
+
ShortLivedResponse.new(*response.body.values_at("access_token", "user_id", "error_type", "error_message", "code"))
|
23
23
|
end
|
24
24
|
|
25
25
|
def exchange_access_token(access_token)
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
module Threads
|
4
|
+
module API
|
5
|
+
class Thread
|
6
|
+
class List
|
7
|
+
attr_reader :threads, :before, :after
|
8
|
+
|
9
|
+
def initialize(json)
|
10
|
+
@threads = json["data"].map { |t| Threads::API::Thread.new(t) }
|
11
|
+
|
12
|
+
@before = json.dig("paging", "cursors", "before")
|
13
|
+
@after = json.dig("paging", "cursors", "after")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :id, :type, :media_url, :permalink, :user_id, :username, :text, :timestamp, :created_at, :shortcode, :video_thumbnail_url, :children
|
18
|
+
|
19
|
+
def initialize(json)
|
20
|
+
@id = json["id"]
|
21
|
+
@type = json["media_type"]
|
22
|
+
@permalink = json["permalink"]
|
23
|
+
@shortcode = json["shortcode"]
|
24
|
+
@text = json["text"]
|
25
|
+
@media_url = json["media_url"]
|
26
|
+
@video_thumbnail_url = json["thumbnail_url"]
|
27
|
+
@user_id = json.dig("owner", "id")
|
28
|
+
@username = json["username"]
|
29
|
+
@is_quote_post = json["is_quote_post"]
|
30
|
+
|
31
|
+
@timestamp = json["timestamp"]
|
32
|
+
@created_at = Time.iso8601(@timestamp) if @timestamp
|
33
|
+
|
34
|
+
children = Array(json["children"])
|
35
|
+
@children = children.map { |c| Thread.new(c) } if children.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def quote_post?
|
39
|
+
@is_quote_post
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class UnpublishedThread
|
44
|
+
attr_reader :id
|
45
|
+
|
46
|
+
def initialize(json)
|
47
|
+
@id = json["id"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class ThreadStatus < UnpublishedThread
|
52
|
+
attr_reader :status, :error_message
|
53
|
+
|
54
|
+
def initialize(json)
|
55
|
+
super
|
56
|
+
|
57
|
+
@status = json["status"]
|
58
|
+
@error_message = json["error_message"]
|
59
|
+
end
|
60
|
+
|
61
|
+
def in_progress?
|
62
|
+
@status == "IN_PROGRESS"
|
63
|
+
end
|
64
|
+
|
65
|
+
def finished?
|
66
|
+
@status == "FINISHED"
|
67
|
+
end
|
68
|
+
|
69
|
+
def published?
|
70
|
+
@status == "PUBLISHED"
|
71
|
+
end
|
72
|
+
|
73
|
+
def errored?
|
74
|
+
@status == "ERROR"
|
75
|
+
end
|
76
|
+
|
77
|
+
def expired?
|
78
|
+
@status == "EXPIRED"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/threads/api/version.rb
CHANGED
data/lib/threads/api.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: threads-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Celis
|
@@ -37,7 +37,9 @@ files:
|
|
37
37
|
- README.md
|
38
38
|
- Rakefile
|
39
39
|
- lib/threads/api.rb
|
40
|
+
- lib/threads/api/client.rb
|
40
41
|
- lib/threads/api/oauth2/client.rb
|
42
|
+
- lib/threads/api/thread.rb
|
41
43
|
- lib/threads/api/version.rb
|
42
44
|
homepage: https://github.com/davidcelis/threads-api
|
43
45
|
licenses:
|