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 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: