threads-api 0.0.1.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|