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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ddb297052b28cab7f6a56c010ede66c4a1ef818a8d0862da7fb365856f482b0
4
- data.tar.gz: ed7000a484974ebb0dc2d66b87db2c59538f50f20616a54f64882f32c52523d5
3
+ metadata.gz: 7bc7d9996e4a119f65407f455788ff761710855ceb5e8d01eae7853a0d5c8022
4
+ data.tar.gz: d13d7280d9f19a53cc3acca6d58bb7871e5f1faa5e93767adfb4e3a2f8a9a83c
5
5
  SHA512:
6
- metadata.gz: d4f1504a2956ab19bd2133fc780cb6f679c0a2ec7ddc3c58ba75e2af99f3f0cf5899067f2909b58f1483cb4d55a8d094b089e5a1347140b6275518efdf0e575c
7
- data.tar.gz: 5e45860b0c861e477d5812fede1bdc4d1a0619cc30a0563442c6bd83001f73c7b29425f43665e8777f7439ffbb600fea6574a3893c54631a22050cdccd3cd81f
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Threads
4
4
  module API
5
- VERSION = "0.0.1.pre"
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
data/lib/threads/api.rb CHANGED
@@ -2,5 +2,6 @@
2
2
 
3
3
  require "faraday"
4
4
 
5
+ require_relative "api/client"
5
6
  require_relative "api/oauth2/client"
6
7
  require_relative "api/version"
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.0.1.pre
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: