omnisocials 0.4.0 → 0.5.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: a8be79cfb8525cbb823f24a4a34d6cb3d51b193d864db80cc190bc0a7db45a18
4
- data.tar.gz: ed95b24b6e722f29e9c7e94e9d4506c69dece3579f9e609966fa999d5d1893bf
3
+ metadata.gz: 2dff156c383d34ece380b804eb9dc42fd00bd42782218bd99e7f840dfc4c197e
4
+ data.tar.gz: 8a7d23f25f08551a42e4b5ad30dc680f2b83045f72bb78c894a9ab13134df8e1
5
5
  SHA512:
6
- metadata.gz: 2138c0350aea5e6c3059bf5d71bc9cd050441ac11b8846d01d55f9c6bd88c83488cd4e47b5c17ba8e298f98c01d75e53dcf1e54845a89a2c92553f4e896a9386
7
- data.tar.gz: 10177239235e7dcd623615ed273c4171b81c2819de790bdc636ff495a1dd3b177cc18011b4cf9ac67a1162358d711d927d38a07c7a00ed9d672a0fe551dc524d
6
+ metadata.gz: 3f3ca94edd9dbac14a91c91fc17c39b22efda2385dc36fd6fbc02b881c2f86a661e672a55665e97c47d4a8ee278ed6cb0e9bd961f4dcba495b936e6840316b88
7
+ data.tar.gz: 9f99feca0fef8165c5d80901cc60f4a3e8f681025e8c786e2e32938582e4194c9a98c8ba60ca54d1166e603bbc3c1f1e751dbdc580e42e9a3032bd650760511a
data/README.md CHANGED
@@ -103,9 +103,9 @@ post = client.posts.create(
103
103
 
104
104
  Note that `linkedin` targets a personal LinkedIn profile and `linkedin_page` targets a LinkedIn company page. Both can be connected to the same workspace and posted to independently.
105
105
 
106
- ### X thread example
106
+ ### Chained threads (X, Bluesky, Mastodon, Threads)
107
107
 
108
- Pass 2 to 25 `thread_parts` to publish a chained thread instead of a single tweet (each part is at most 280 characters). Bluesky and Mastodon support the same `thread_parts` shape:
108
+ Pass 2 to 25 `thread_parts` to publish a chained thread instead of a single tweet (each part is at most 280 characters). Bluesky, Mastodon and Threads support the same `thread_parts` shape (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; parts after the first publish as replies to the previous part, and the Threads caption is taken from part 1):
109
109
 
110
110
  ```ruby
111
111
  post = client.posts.create(
@@ -123,7 +123,24 @@ post = client.posts.create(
123
123
  )
124
124
  ```
125
125
 
126
- On update, passing `x: { "thread_parts" => nil }` clears the thread and reverts the post to single-tweet mode. Only top-level `nil` values are dropped from request bodies, so nested `nil` values like this one are sent as JSON `null`.
126
+ ```ruby
127
+ # Meta Threads chain with a carousel on the first part
128
+ post = client.posts.create(
129
+ content: "Behind the scenes of our summer shoot",
130
+ channels: ["threads"],
131
+ threads: {
132
+ "thread_parts" => [
133
+ { "text" => "Behind the scenes of our summer shoot. A few highlights:", "media_urls" => ["https://example.com/shoot-1.jpg", "https://example.com/shoot-2.jpg"] },
134
+ { "text" => "Day one: scouting locations at sunrise." },
135
+ { "text" => "Day two: the full crew, 14 hours, zero regrets." }
136
+ ]
137
+ }
138
+ )
139
+ ```
140
+
141
+ On update, passing `x: { "thread_parts" => nil }` clears the thread and reverts the post to single-tweet mode (same for `bluesky`, `mastodon` and `threads`). Only top-level `nil` values are dropped from request bodies, so nested `nil` values like this one are sent as JSON `null`.
142
+
143
+ Threads posts can also carry a location tag: pass `threads: { "location_id" => "..." }` with an id from `client.locations.search(platform: "threads")` (see Locations below). On a multi-post thread the tag is applied to part 1, and on update `threads: { "location_id" => nil }` clears it. Threads location tagging is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error.
127
144
 
128
145
  ### X link posts use credits
129
146
 
@@ -182,6 +199,15 @@ recent = client.posts.recent_platform(limit: 10, platforms: ["instagram", "tikto
182
199
 
183
200
  `retry` re-publishes only the platforms that failed, on the same post; platforms that already succeeded are never posted again. It is asynchronous: a 200 means the retry is queued, so poll `get` for the outcome. Max 3 retries per platform.
184
201
 
202
+ ### Approve or reject a post
203
+
204
+ ```ruby
205
+ client.posts.approve("123") # approve the current approval-workflow step
206
+ client.posts.reject("123", comment: "Wrong CTA link, please fix.") # reject and stop the workflow (comment optional)
207
+ ```
208
+
209
+ Only works on a post with `approval_status: "pending"` (`status: "in_approval"`). Both act on behalf of the user who owns the API key, who must be a listed approver for the workflow's CURRENT step — steps approve in order, so being an approver on a later step is not enough yet (raises a 403 `OmniSocials::PermissionDeniedError`). Approving the last step finalizes the post (`scheduled` or `posting`); rejecting stops the whole workflow immediately, not just the current step.
210
+
185
211
  ## Media
186
212
 
187
213
  ### Upload from a URL (recommended, up to 1GB)
@@ -315,7 +341,7 @@ best = client.analytics.best_times(platform: "instagram", timezone: "Europe/Amst
315
341
  best["data"]["best_times"].each { |slot| puts slot }
316
342
  ```
317
343
 
318
- ## Locations (Instagram place tagging)
344
+ ## Locations (Instagram and Threads place tagging)
319
345
 
320
346
  ```ruby
321
347
  results = client.locations.search("Blue Bottle Coffee Oakland")
@@ -330,6 +356,23 @@ client.posts.create(
330
356
  )
331
357
  ```
332
358
 
359
+ Threads uses its own location ids (a Facebook Place ID is not a Threads location id). Pass `platform: "threads"` and search by keyword, or by `latitude` plus `longitude` instead of `q`; use a result's `id` as `threads.location_id` on a post:
360
+
361
+ ```ruby
362
+ results = client.locations.search("Blue Bottle Coffee Oakland", platform: "threads")
363
+ # or around a point instead of a keyword:
364
+ results = client.locations.search(platform: "threads", latitude: 37.8044, longitude: -122.2712)
365
+ threads_location_id = results["locations"][0]["id"]
366
+
367
+ client.posts.create(
368
+ content: "Great coffee here",
369
+ channels: ["threads"],
370
+ threads: { "location_id" => threads_location_id }
371
+ )
372
+ ```
373
+
374
+ The Threads response is `{ "locations" => [...] }` (each with nullable `name`, `address`, `city`, `country`, `latitude`, `longitude`) or `{ "error" => { "code", "message" } }` with `code` one of `not_available`, `threads_not_connected`, `threads_reauth_required` (reconnect Threads), or `platform_error`. Threads location tagging is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error.
375
+
333
376
  ## Inbox
334
377
 
335
378
  Read and reply to social inbox conversations (DMs, comments, mentions) across connected platforms. Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes. The list endpoints are cursor-paginated (unlike the offset pagination used elsewhere): page while `pagination["has_more"]` is true by passing the previous response's `pagination["next_cursor"]` as `cursor`.
@@ -348,9 +391,14 @@ end
348
391
 
349
392
  client.inbox.mark_read(conversation_id)
350
393
  client.inbox.reply(conversation_id, text: "Thanks for reaching out!")
394
+
395
+ # Threads only: hide or unhide a reply someone left on one of your Threads posts.
396
+ message_id = messages["data"][0]["id"]
397
+ client.inbox.hide(message_id) # hide
398
+ client.inbox.hide(message_id, hide: false) # unhide
351
399
  ```
352
400
 
353
- `platform` accepts `"instagram"`, `"facebook"`, `"linkedin"`, `"tiktok"`, or `"x"`; `type` accepts `"dm"`, `"comment"`, or `"mention"`. TikTok replies are comments only and capped at 150 characters. Conversation ids are URL-encoded for you, so pass them exactly as returned - LinkedIn ids contain `":"` and `"()"` (e.g. `"linkedin_comment_urn:li:activity:123"`).
401
+ `platform` accepts `"instagram"`, `"facebook"`, `"linkedin"`, `"tiktok"`, `"youtube"`, `"x"`, or `"threads"`; `type` accepts `"dm"`, `"comment"`, or `"mention"`. Threads conversations are comments (replies people leave on your Threads posts) and mentions; there are no Threads DMs. Only incoming top-level Threads replies can be hidden (nested replies cannot), and a hidden message keeps its place in the conversation with its `hidden` flag set. Threads inbox is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error, and it needs a Threads connection with the reply permission (a 401 `reauth_required` means reconnect Threads). TikTok and YouTube replies are comments only; TikTok replies are capped at 150 characters. Conversation ids are URL-encoded for you, so pass them exactly as returned - LinkedIn ids contain `":"` and `"()"` (e.g. `"linkedin_comment_urn:li:activity:123"`).
354
402
 
355
403
  Replying to an X DM costs 2 prepaid credits, debited from the company balance before the send and automatically refunded if the send fails:
356
404
 
@@ -23,10 +23,18 @@ module OmniSocials
23
23
  # GET /inbox/conversations - list conversations, newest activity first.
24
24
  #
25
25
  # All filters are optional: platform ("instagram", "facebook",
26
- # "linkedin", "tiktok", "x"), type ("dm", "comment", "mention"),
27
- # unread (only conversations with unread messages), limit (1-100), and
28
- # cursor (an opaque cursor from a previous response's
26
+ # "linkedin", "tiktok", "youtube", "x", "threads"), type ("dm", "comment",
27
+ # "mention"), unread (only conversations with unread messages), limit
28
+ # (1-100), and cursor (an opaque cursor from a previous response's
29
29
  # pagination["next_cursor"]).
30
+ #
31
+ # Threads conversations are type "comment" (replies people leave on the
32
+ # user's Threads posts; conversation ids look like
33
+ # "threads_comment_<rootPostId>") and "mention"
34
+ # ("threads_mention_<postId>"); there are no Threads DMs. Threads inbox
35
+ # is currently rolling out: until Meta approves the permissions it is
36
+ # disabled on production and calls return a clear error, and it needs a
37
+ # Threads connection with the reply permission.
30
38
  def list_conversations(platform: nil, type: nil, unread: nil, limit: nil, cursor: nil)
31
39
  @client.request(
32
40
  "GET", "/inbox/conversations",
@@ -58,9 +66,17 @@ module OmniSocials
58
66
  # POST /inbox/conversations/{id}/reply - send a reply into the
59
67
  # conversation (a DM message, or a reply to the comment/mention).
60
68
  #
61
- # `text` is required. Optionally attach a single media asset by public
62
- # URL with `attachment_url` plus `attachment_type` ("image", "video",
63
- # "audio", or "file"). Returns the created outgoing message.
69
+ # On Facebook and Instagram DMs, optionally attach a single media asset
70
+ # by public URL with `attachment_url` plus `attachment_type` ("image",
71
+ # "video", "audio", or "file"); `text` is optional when `attachment_url`
72
+ # is set (an attachment-only reply is allowed). Other platforms are
73
+ # text-only. Returns the created outgoing message.
74
+ #
75
+ # On a Threads conversation the reply publishes as a native Threads
76
+ # reply. Threads inbox is currently rolling out (disabled on production
77
+ # until Meta App Review) and needs a Threads connection with the reply
78
+ # permission: a 401 with code "reauth_required" means the connection
79
+ # lacks that permission (reconnect Threads).
64
80
  #
65
81
  # Replying to an X DM costs 2 prepaid credits, debited from the
66
82
  # company balance before the send and automatically refunded if the
@@ -70,7 +86,7 @@ module OmniSocials
70
86
  # auto-suspended after the balance hit zero (top up and re-enable it
71
87
  # in the dashboard to resume; DMs that arrived while suspended are
72
88
  # not recovered).
73
- def reply(conversation_id, text:, attachment_url: nil, attachment_type: nil)
89
+ def reply(conversation_id, text: nil, attachment_url: nil, attachment_type: nil)
74
90
  body = Internal.drop_nil(
75
91
  {
76
92
  "text" => text,
@@ -84,11 +100,31 @@ module OmniSocials
84
100
  )
85
101
  end
86
102
 
103
+ # POST /inbox/messages/{id}/hide - hide or unhide a reply someone left
104
+ # on one of the user's Threads posts, as the post owner (Threads only
105
+ # for now). Pass hide: false to unhide. Only incoming top-level replies
106
+ # can be hidden (Threads does not allow hiding nested replies); the
107
+ # message keeps its place in the conversation. Returns the updated
108
+ # message with its "hidden" flag flipped.
109
+ #
110
+ # Errors: 400 "unsupported_platform" (not an incoming Threads reply, or
111
+ # Threads inbox not available yet), 400 "not_hideable" (nested reply or
112
+ # Threads refused), 401 "reauth_required" (connection lacks the reply
113
+ # permission; reconnect Threads), 404 "not_found" (message not in this
114
+ # workspace) or "account_not_connected" (no Threads account).
115
+ def hide(message_id, hide: true)
116
+ @client.request(
117
+ "POST", "/inbox/messages/#{encode_id(message_id)}/hide",
118
+ json: { "hide" => hide }
119
+ )
120
+ end
121
+
87
122
  private
88
123
 
89
- # URL-encode a conversation id for use in a path segment. LinkedIn ids
90
- # contain ":" and "()", so they must be escaped; spaces become %20
91
- # (a path segment treats "+" literally, unlike a query string).
124
+ # URL-encode a conversation or message id for use in a path segment.
125
+ # LinkedIn conversation ids contain ":" and "()", so they must be
126
+ # escaped; spaces become %20 (a path segment treats "+" literally,
127
+ # unlike a query string).
92
128
  def encode_id(conversation_id)
93
129
  CGI.escape(conversation_id.to_s).gsub("+", "%20")
94
130
  end
@@ -2,16 +2,48 @@
2
2
 
3
3
  module OmniSocials
4
4
  module Resources
5
- # Locations resource: Instagram place tagging (search + validate).
5
+ # Locations resource: Instagram and Threads place tagging (search +
6
+ # validate).
6
7
  class Locations
7
8
  def initialize(client)
8
9
  @client = client
9
10
  end
10
11
 
11
- # GET /locations/search?q= - search Facebook place pages usable as an
12
- # Instagram location_id.
13
- def search(q)
14
- @client.request("GET", "/locations/search", query: { "q" => q })
12
+ # GET /locations/search - search locations for place tagging.
13
+ #
14
+ # platform is "instagram" (default) or "threads". The two sources use
15
+ # DIFFERENT ids: a Facebook Place ID is not a Threads location id.
16
+ #
17
+ # Instagram: pass q to search Facebook place pages usable as an
18
+ # Instagram location_id. Response: { "data" => [...] } plus optional
19
+ # "error" (a plain string on the degraded path) and "needsPermission".
20
+ #
21
+ # Threads: pass q, OR latitude (-90..90) plus longitude (-180..180) to
22
+ # search around a point instead of q. Response:
23
+ # { "locations" => [{ id, name, address, city, country, latitude,
24
+ # longitude }] } (all fields but id nullable), or
25
+ # { "error" => { "code", "message" } } where code is one of
26
+ # "not_available" (Threads location tagging not enabled in this
27
+ # environment yet), "threads_not_connected", "threads_reauth_required"
28
+ # (the connection lacks the threads_location_tagging permission;
29
+ # reconnect Threads), or "platform_error". Validation problems (neither
30
+ # q nor lat+lng, q under 2 chars, coordinates out of range) raise a 400
31
+ # with the standard error envelope. Pass a result's id as
32
+ # threads.location_id on post create/update.
33
+ #
34
+ # Threads location tagging is currently rolling out: until Meta
35
+ # approves the permissions it is disabled on production and calls
36
+ # return a clear error.
37
+ def search(q = nil, platform: nil, latitude: nil, longitude: nil)
38
+ @client.request(
39
+ "GET", "/locations/search",
40
+ query: {
41
+ "q" => q,
42
+ "platform" => platform,
43
+ "latitude" => latitude,
44
+ "longitude" => longitude
45
+ }
46
+ )
15
47
  end
16
48
 
17
49
  # GET /locations/validate?id= - validate a location id before attaching
@@ -11,8 +11,8 @@ module OmniSocials
11
11
  # for media_urls, `{ "id" => "...", "alt" => "..." }` for media_ids. Alt
12
12
  # text is delivered to Mastodon (media description), Bluesky (embed alt),
13
13
  # X (photos/GIFs), Pinterest (pin alt text), Instagram (images), and
14
- # LinkedIn (images); the same entry shape works inside x/bluesky/mastodon
15
- # `thread_parts` media.
14
+ # LinkedIn (images); the same entry shape works inside
15
+ # x/bluesky/mastodon/threads `thread_parts` media.
16
16
  class Posts
17
17
  def initialize(client)
18
18
  @client = client
@@ -67,6 +67,18 @@ module OmniSocials
67
67
  # company's total reserved credits past its balance. Drafts are never
68
68
  # gated, and posts scheduled to publish before 2026-08-14 are never
69
69
  # gated either.
70
+ #
71
+ # Threads posts can carry a location tag: pass
72
+ # threads: { "location_id" => "..." } (an id from locations.search with
73
+ # platform: "threads"), or threads: { "location" => { "id" => "...",
74
+ # "name" => "..." } } to store display fields along with the id
75
+ # (location_id wins when both are given). On a multi-post thread
76
+ # (thread_parts) the tag is applied to part 1, and the Post's "threads"
77
+ # block echoes a "location" object when set. Threads location tagging
78
+ # is currently rolling out: until Meta approves the permissions it is
79
+ # disabled on production and create/update/publish return a 400 (also a
80
+ # 400 validation_error asking you to reconnect Threads when the
81
+ # connection lacks the threads_location_tagging permission).
70
82
  def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
71
83
  media_urls: nil, type: nil, source: nil, link_url: nil,
72
84
  link_title: nil, link_description: nil, link_thumbnail_url: nil,
@@ -75,7 +87,8 @@ module OmniSocials
75
87
  hashtag_platforms: nil, pinterest: nil, youtube: nil,
76
88
  instagram: nil, facebook: nil, linkedin: nil,
77
89
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
78
- mastodon: nil, google_business: nil, linkedin_poll: nil)
90
+ mastodon: nil, threads: nil, google_business: nil,
91
+ linkedin_poll: nil)
79
92
  body = create_body(
80
93
  content: content, channels: channels, scheduled_at: scheduled_at,
81
94
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -87,7 +100,7 @@ module OmniSocials
87
100
  hashtag_platforms: hashtag_platforms, pinterest: pinterest,
88
101
  youtube: youtube, instagram: instagram, facebook: facebook,
89
102
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
90
- x: x, bluesky: bluesky, mastodon: mastodon,
103
+ x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
91
104
  google_business: google_business, linkedin_poll: linkedin_poll
92
105
  )
93
106
  @client.request("POST", "/posts/create", json: body)
@@ -106,7 +119,8 @@ module OmniSocials
106
119
  pinterest: nil, youtube: nil, instagram: nil,
107
120
  facebook: nil, linkedin: nil, linkedin_page: nil,
108
121
  tiktok: nil, x: nil, bluesky: nil, mastodon: nil,
109
- google_business: nil, linkedin_poll: nil)
122
+ threads: nil, google_business: nil,
123
+ linkedin_poll: nil)
110
124
  body = create_body(
111
125
  content: content, channels: channels, scheduled_at: nil,
112
126
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -118,7 +132,7 @@ module OmniSocials
118
132
  hashtag_platforms: hashtag_platforms, pinterest: pinterest,
119
133
  youtube: youtube, instagram: instagram, facebook: facebook,
120
134
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
121
- x: x, bluesky: bluesky, mastodon: mastodon,
135
+ x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
122
136
  google_business: google_business, linkedin_poll: linkedin_poll
123
137
  )
124
138
  @client.request("POST", "/posts/create-and-publish", json: body)
@@ -128,8 +142,9 @@ module OmniSocials
128
142
  #
129
143
  # Only top-level nils are dropped from the body, so passing e.g.
130
144
  # x: { "thread_parts" => nil } still clears an X thread (reverts the
131
- # post to single-tweet mode). The same applies to bluesky and mastodon
132
- # thread parts.
145
+ # post to single-tweet mode). The same applies to bluesky, mastodon
146
+ # and threads thread parts, and to a Threads location tag:
147
+ # threads: { "location_id" => nil } (or "location" => nil) clears it.
133
148
  #
134
149
  # See #create for the 402 "x_credits_insufficient" credit gate that
135
150
  # can also refuse an update to a scheduled X link post.
@@ -138,7 +153,8 @@ module OmniSocials
138
153
  collaborators: nil, user_tags: nil, pinterest: nil,
139
154
  youtube: nil, instagram: nil, facebook: nil, linkedin: nil,
140
155
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
141
- mastodon: nil, google_business: nil, linkedin_poll: nil)
156
+ mastodon: nil, threads: nil, google_business: nil,
157
+ linkedin_poll: nil)
142
158
  body = Internal.drop_nil(
143
159
  {
144
160
  "content" => content,
@@ -160,6 +176,7 @@ module OmniSocials
160
176
  "x" => x,
161
177
  "bluesky" => bluesky,
162
178
  "mastodon" => mastodon,
179
+ "threads" => threads,
163
180
  "google_business" => google_business,
164
181
  "linkedin_poll" => linkedin_poll
165
182
  }
@@ -190,6 +207,29 @@ module OmniSocials
190
207
  @client.request("POST", "/posts/#{post_id}/retry")
191
208
  end
192
209
 
210
+ # POST /posts/{id}/approve - approve the current step of a post's
211
+ # approval workflow, on behalf of the user who owns this API key. That
212
+ # user must be a listed approver for the workflow's CURRENT step -
213
+ # steps approve in order, so an approver on a later step gets a 403
214
+ # "forbidden" error until earlier steps clear. Only works on a post
215
+ # with approval_status "pending". If this is the last step, the post
216
+ # finalizes immediately ("scheduled" or "posting"); otherwise it stays
217
+ # "in_approval" and the next step's approvers are notified.
218
+ def approve(post_id)
219
+ @client.request("POST", "/posts/#{post_id}/approve")
220
+ end
221
+
222
+ # POST /posts/{id}/reject - reject a post's approval workflow, on
223
+ # behalf of the user who owns this API key. Same approver requirement
224
+ # as #approve. Unlike approval, a rejection stops the WHOLE workflow
225
+ # immediately (not just the current step) - the post's status becomes
226
+ # "rejected". `comment` is optional and, when given, is shown to the
227
+ # requester and other approvers in the post's review thread.
228
+ def reject(post_id, comment: nil)
229
+ body = comment ? { comment: comment } : nil
230
+ @client.request("POST", "/posts/#{post_id}/reject", json: body)
231
+ end
232
+
193
233
  private
194
234
 
195
235
  def create_body(content:, channels:, scheduled_at:, media_ids:,
@@ -199,7 +239,7 @@ module OmniSocials
199
239
  hashtag_set_id:, hashtag_placement:, hashtag_platforms:,
200
240
  pinterest:, youtube:, instagram:, facebook:, linkedin:,
201
241
  linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
202
- google_business:, linkedin_poll:)
242
+ threads:, google_business:, linkedin_poll:)
203
243
  Internal.drop_nil(
204
244
  {
205
245
  "content" => content,
@@ -230,6 +270,7 @@ module OmniSocials
230
270
  "x" => x,
231
271
  "bluesky" => bluesky,
232
272
  "mastodon" => mastodon,
273
+ "threads" => threads,
233
274
  "google_business" => google_business,
234
275
  "linkedin_poll" => linkedin_poll
235
276
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnisocials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OmniSocials
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-22 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Schedule and publish social media posts, upload media, and read analytics
14
14
  across Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads,