omnisocials 0.5.0 → 0.6.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: 2dff156c383d34ece380b804eb9dc42fd00bd42782218bd99e7f840dfc4c197e
4
- data.tar.gz: 8a7d23f25f08551a42e4b5ad30dc680f2b83045f72bb78c894a9ab13134df8e1
3
+ metadata.gz: 4b3fed727cadc18b6ea190937598241ea06e5ac3451f5a01e47e993f641f5332
4
+ data.tar.gz: 6adeeba6ddc69911b79c4b620ddc418e0dd372d6e76e3a417bd285ae54b44e9d
5
5
  SHA512:
6
- metadata.gz: 3f3ca94edd9dbac14a91c91fc17c39b22efda2385dc36fd6fbc02b881c2f86a661e672a55665e97c47d4a8ee278ed6cb0e9bd961f4dcba495b936e6840316b88
7
- data.tar.gz: 9f99feca0fef8165c5d80901cc60f4a3e8f681025e8c786e2e32938582e4194c9a98c8ba60ca54d1166e603bbc3c1f1e751dbdc580e42e9a3032bd650760511a
6
+ metadata.gz: ad4c11a0f91aa25cd0dfab676f0a28b5973d139a82137c0d753de697a453d710bd9f59f6c212596230fe727e37d20e117635d7b23d06f2f6aeeefce3d66b5ffa
7
+ data.tar.gz: 1b53388871737de150a7d71387472915b3e1bfcc7c8e245f45e5c3d87ea4ef2ee9d8047855876d33e1028c0b24272accfe663ef482e83bbdc4e60a72fe9268af
data/README.md CHANGED
@@ -105,7 +105,7 @@ Note that `linkedin` targets a personal LinkedIn profile and `linkedin_page` tar
105
105
 
106
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, 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):
108
+ Pass 2 to 25 `thread_parts` to publish a chained thread instead of a single tweet (each part is at most 280 characters, 25,000 for X Premium/Premium+ accounts). 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(
@@ -392,13 +392,29 @@ end
392
392
  client.inbox.mark_read(conversation_id)
393
393
  client.inbox.reply(conversation_id, text: "Thanks for reaching out!")
394
394
 
395
- # Threads only: hide or unhide a reply someone left on one of your Threads posts.
395
+ # Hide or unhide a comment someone left on one of your posts (Facebook,
396
+ # Instagram, TikTok, YouTube, Threads). Delete removes it outright (Facebook,
397
+ # Instagram, TikTok; YouTube: hide instead), replies under it included.
396
398
  message_id = messages["data"][0]["id"]
397
399
  client.inbox.hide(message_id) # hide
398
400
  client.inbox.hide(message_id, hide: false) # unhide
401
+ client.inbox.delete_message(message_id)
399
402
  ```
400
403
 
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"`).
404
+ `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. Comments can be hidden on Facebook, Instagram, TikTok, YouTube, and Threads (Threads: incoming top-level replies only), and a hidden message keeps its place in the conversation with its `hidden` flag set; `hidden` is `true`/`false` on comments and `nil` on DMs. A comment/mention's `post` carries `url` (public link when the platform provides one) and `media_type` (the platform's own label) next to `id`, `caption`, and `thumbnail`. 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"`).
405
+
406
+ `next` hands out the next conversation that still needs a reply (the customer's latest DM with no reply after it, or an unreplied comment/mention that is not hidden), with the whole thread and the post it belongs to, so a reply can be drafted from one call. Replies typed in the native apps count as answers. Only unread items are served by default, so `mark_read` is the durable way to skip one; `exclude` skips conversation ids for the current session only. Pass `include_next: true` to `reply` to get the following item in the same response. `list_conversations(unanswered: true)` gives the same set as a plain list.
407
+
408
+ ```ruby
409
+ item = client.inbox.next(platform: "instagram")
410
+ while item["data"]
411
+ message = item["data"]["message"]
412
+ puts "#{item["remaining"]} left. #{message["sender"]["username"]}: #{message["text"]}"
413
+
414
+ reply = client.inbox.reply(message["conversation_id"], text: "Thanks! DM sent.", include_next: true)
415
+ item = { "data" => reply["next"], "remaining" => reply["remaining"] || 0 }
416
+ end
417
+ ```
402
418
 
403
419
  Replying to an X DM costs 2 prepaid credits, debited from the company balance before the send and automatically refunded if the send fails:
404
420
 
@@ -59,8 +59,8 @@ module OmniSocials
59
59
  ].freeze
60
60
 
61
61
  attr_reader :api_key, :base_url, :timeout, :max_retries,
62
- :posts, :media, :folders, :hashtag_sets, :accounts, :analytics,
63
- :audio, :locations, :inbox, :webhooks
62
+ :posts, :media, :folders, :hashtag_sets, :approval_workflows,
63
+ :accounts, :analytics, :audio, :locations, :inbox, :webhooks
64
64
 
65
65
  # api_key - API key (omsk_live_* / omsk_test_*). Falls back to the
66
66
  # OMNISOCIALS_API_KEY environment variable. Raises
@@ -89,6 +89,7 @@ module OmniSocials
89
89
  @media = Resources::Media.new(self)
90
90
  @folders = Resources::Folders.new(self)
91
91
  @hashtag_sets = Resources::HashtagSets.new(self)
92
+ @approval_workflows = Resources::ApprovalWorkflows.new(self)
92
93
  @accounts = Resources::Accounts.new(self)
93
94
  @analytics = Resources::Analytics.new(self)
94
95
  @audio = Resources::Audio.new(self)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniSocials
4
+ module Resources
5
+ # Approval workflows resource: the workflows configured in the dashboard
6
+ # (Approvals). List them here and route a post through one at create time
7
+ # via approval_workflow_id on posts.create.
8
+ class ApprovalWorkflows
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ # GET /approval-workflows - the workflows this workspace can use
14
+ # (company-wide plus workspace-bound), with steps and named approvers.
15
+ def list
16
+ @client.request("GET", "/approval-workflows")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -24,24 +24,29 @@ module OmniSocials
24
24
  #
25
25
  # All filters are optional: platform ("instagram", "facebook",
26
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
- # pagination["next_cursor"]).
27
+ # "mention"), unread (only conversations with unread messages),
28
+ # unanswered (only conversations that still need an answer: the
29
+ # customer's latest DM has no reply after it, for Instagram/Facebook DMs
30
+ # within the 24-hour messaging window only, or a comment/mention that
31
+ # has not been replied to and is not hidden; replies typed in the native
32
+ # apps count as answers, and read state is ignored, so use `next` for a
33
+ # work queue), limit (1-100), and cursor (an opaque cursor from a
34
+ # previous response's pagination["next_cursor"]).
30
35
  #
31
36
  # Threads conversations are type "comment" (replies people leave on the
32
37
  # user's Threads posts; conversation ids look like
33
38
  # "threads_comment_<rootPostId>") and "mention"
34
39
  # ("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.
38
- def list_conversations(platform: nil, type: nil, unread: nil, limit: nil, cursor: nil)
40
+ # needs a Threads connection with the reply permissions; connections made
41
+ # before those permissions existed must be reconnected once.
42
+ def list_conversations(platform: nil, type: nil, unread: nil, unanswered: nil, limit: nil, cursor: nil)
39
43
  @client.request(
40
44
  "GET", "/inbox/conversations",
41
45
  query: {
42
46
  "platform" => platform,
43
47
  "type" => type,
44
48
  "unread" => unread,
49
+ "unanswered" => unanswered,
45
50
  "limit" => limit,
46
51
  "cursor" => cursor
47
52
  }
@@ -73,10 +78,9 @@ module OmniSocials
73
78
  # text-only. Returns the created outgoing message.
74
79
  #
75
80
  # 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
81
+ # reply. The Threads inbox needs a Threads connection with the reply
78
82
  # permission: a 401 with code "reauth_required" means the connection
79
- # lacks that permission (reconnect Threads).
83
+ # lacks that permission (connected before it existed; reconnect Threads).
80
84
  #
81
85
  # Replying to an X DM costs 2 prepaid credits, debited from the
82
86
  # company balance before the send and automatically refunded if the
@@ -86,12 +90,19 @@ module OmniSocials
86
90
  # auto-suspended after the balance hit zero (top up and re-enable it
87
91
  # in the dashboard to resume; DMs that arrived while suspended are
88
92
  # not recovered).
89
- def reply(conversation_id, text: nil, attachment_url: nil, attachment_type: nil)
93
+ #
94
+ # Pass include_next: true to also get "next" (the next conversation
95
+ # that needs an answer, the same object `next` returns under "data",
96
+ # using its default queue order and filters; nil when nothing is
97
+ # waiting) and "remaining" in the response. Saves the extra call when
98
+ # working through the inbox.
99
+ def reply(conversation_id, text: nil, attachment_url: nil, attachment_type: nil, include_next: nil)
90
100
  body = Internal.drop_nil(
91
101
  {
92
102
  "text" => text,
93
103
  "attachment_url" => attachment_url,
94
- "attachment_type" => attachment_type
104
+ "attachment_type" => attachment_type,
105
+ "include_next" => include_next
95
106
  }
96
107
  )
97
108
  @client.request(
@@ -100,18 +111,31 @@ module OmniSocials
100
111
  )
101
112
  end
102
113
 
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.
114
+ # POST /inbox/messages/{id}/hide - hide or unhide a comment someone
115
+ # left on one of the user's posts, on the platform, as the post owner.
116
+ # Facebook, Instagram, TikTok, YouTube and Threads comments (Threads:
117
+ # incoming top-level replies only; Threads does not allow hiding nested
118
+ # replies). Pass hide: false to unhide. On YouTube, hide sets the
119
+ # comment's moderation status to rejected, which removes it and its
120
+ # replies from public view; unhide publishes it again. Returns the
121
+ # updated message with its "hidden" flag flipped; the message keeps its
122
+ # place in the conversation, and a hidden comment no longer counts as
123
+ # unanswered. The account must have been connected with the moderation
124
+ # permission (Facebook pages_manage_engagement, Instagram
125
+ # instagram_business_manage_comments).
109
126
  #
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).
127
+ # Errors: 400 "unsupported_platform" (not an incoming comment on a
128
+ # supported platform), 400 "not_hideable" (Threads nested reply, or
129
+ # Threads refused), 401 "reauth_required" (the Threads reply permission
130
+ # or the TikTok comments authorization is missing or expired), 403
131
+ # "reconnect_required" (the account was connected without the
132
+ # comment-moderation permission; reconnect it in the dashboard), 404
133
+ # "not_found" (message not in this workspace) or
134
+ # "account_not_connected", 429 "quota_exceeded" (YouTube's daily API
135
+ # quota is used up; retry after midnight Pacific), 502 "platform_error"
136
+ # (the platform rejected the call). Threads inbox is currently rolling
137
+ # out; until Meta approves the permissions it is disabled on production
138
+ # and Threads calls return a clear error.
115
139
  def hide(message_id, hide: true)
116
140
  @client.request(
117
141
  "POST", "/inbox/messages/#{encode_id(message_id)}/hide",
@@ -119,6 +143,75 @@ module OmniSocials
119
143
  )
120
144
  end
121
145
 
146
+ # DELETE /inbox/messages/{id} - delete a comment someone left on one of
147
+ # the user's posts, on the platform and from the inbox. Facebook,
148
+ # Instagram and TikTok comments only: YouTube's API does not let a
149
+ # channel delete other people's comments, hide those instead (`hide`).
150
+ # Replies under the deleted comment go with it (the platforms cascade
151
+ # the delete and the inbox mirrors that); their inbox ids come back as
152
+ # "removed_reply_ids". A comment that is already gone on the platform
153
+ # is still removed from the inbox. This cannot be undone. Returns
154
+ # { "data" => { "id", "conversation_id", "removed_reply_ids" } }.
155
+ #
156
+ # Errors: 400 "unsupported_platform" (not an incoming Facebook,
157
+ # Instagram or TikTok comment), 401 "reauth_required" (the TikTok
158
+ # comments authorization expired), 403 "reconnect_required" (the
159
+ # account was connected without the comment-moderation permission;
160
+ # reconnect it in the dashboard), 404 "not_found" (message not in this
161
+ # workspace) or "account_not_connected", 502 "platform_error" (the
162
+ # platform rejected the call).
163
+ def delete_message(message_id)
164
+ @client.request("DELETE", "/inbox/messages/#{encode_id(message_id)}")
165
+ end
166
+
167
+ # GET /inbox/next - the next conversation that needs an answer: a work
168
+ # queue for answering the inbox. Returns the oldest (by default) item
169
+ # that still needs a reply, together with its conversation so far and
170
+ # the post it belongs to, so a reply can be drafted from one call. An
171
+ # item needs an answer when it is the customer's latest DM with no
172
+ # reply after it (Instagram/Facebook DMs within the 24-hour messaging
173
+ # window only, since Meta refuses replies outside it), or a
174
+ # comment/mention that has not been replied to and is not hidden.
175
+ # Replies typed in the native apps count as answers (they are mirrored
176
+ # into the inbox), so a thread a colleague answered on their phone is
177
+ # not served again. Instagram mentions are skipped (no reply path).
178
+ # Looks at the last 30 days of activity. Requires the inbox:read scope.
179
+ #
180
+ # Only unread items are served by default: marking a conversation read
181
+ # (`mark_read`) is how to skip one for good; pass include_read: true to
182
+ # include read-but-unanswered items. exclude is a session-local skip:
183
+ # conversation ids (an Array, or a comma-separated String) to leave out
184
+ # of this call, up to 100. order is "oldest" (default: the item that
185
+ # has waited longest first) or "newest". platform and type ("dm",
186
+ # "comment", "mention") narrow the queue.
187
+ #
188
+ # Returns { "data" => ..., "remaining" => Integer }. "data" is
189
+ # { "conversation", "message", "messages" }, or nil when nothing is
190
+ # waiting. "message" is the unanswered incoming item itself (the
191
+ # customer's latest DM, or the specific comment): its "id" is what
192
+ # `hide` and `delete_message` take, its "conversation_id" is what
193
+ # `reply` takes. "messages" is the conversation so far, oldest first
194
+ # (the most recent 50 messages for long DM threads). "remaining" is the
195
+ # number of unanswered items still waiting after this one (capped at
196
+ # 500), 0 when "data" is nil. To chain the queue, pass
197
+ # include_next: true to `reply` and it returns the next item in the
198
+ # same response. Errors: 400 "validation_error" (unknown platform, type
199
+ # or order).
200
+ def next(platform: nil, type: nil, order: nil, include_read: nil, exclude: nil)
201
+ exclude = exclude.join(",") if exclude.is_a?(Array)
202
+ exclude = nil if exclude == ""
203
+ @client.request(
204
+ "GET", "/inbox/next",
205
+ query: {
206
+ "platform" => platform,
207
+ "type" => type,
208
+ "order" => order,
209
+ "include_read" => include_read,
210
+ "exclude" => exclude
211
+ }
212
+ )
213
+ end
214
+
122
215
  private
123
216
 
124
217
  # URL-encode a conversation or message id for use in a path segment.
@@ -46,6 +46,13 @@ module OmniSocials
46
46
  # POST /posts/create - create a post (draft, or scheduled when
47
47
  # scheduled_at is set).
48
48
  #
49
+ # approval_workflow_id (a workflow id from client.approval_workflows.list)
50
+ # routes the post through a saved approval workflow: it is created as
51
+ # in_approval (approval_status "pending") instead of scheduled, the
52
+ # approvers are notified, and it publishes at scheduled_at once the last
53
+ # step approves. Requires scheduled_at; not allowed with publish_now.
54
+ # Errors: 404 workflow_not_found, 400 validation_error.
55
+ #
49
56
  # hashtag_set (set name, case-insensitive) or hashtag_set_id applies a
50
57
  # saved hashtag set once at create time; tags already in a caption are
51
58
  # skipped; Instagram's 30-hashtag cap returns error code
@@ -88,7 +95,7 @@ module OmniSocials
88
95
  instagram: nil, facebook: nil, linkedin: nil,
89
96
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
90
97
  mastodon: nil, threads: nil, google_business: nil,
91
- linkedin_poll: nil)
98
+ linkedin_poll: nil, approval_workflow_id: nil)
92
99
  body = create_body(
93
100
  content: content, channels: channels, scheduled_at: scheduled_at,
94
101
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -101,7 +108,8 @@ module OmniSocials
101
108
  youtube: youtube, instagram: instagram, facebook: facebook,
102
109
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
103
110
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
104
- google_business: google_business, linkedin_poll: linkedin_poll
111
+ google_business: google_business, linkedin_poll: linkedin_poll,
112
+ approval_workflow_id: approval_workflow_id
105
113
  )
106
114
  @client.request("POST", "/posts/create", json: body)
107
115
  end
@@ -184,7 +192,7 @@ module OmniSocials
184
192
  @client.request("PATCH", "/posts/#{post_id}", json: body)
185
193
  end
186
194
 
187
- # DELETE /posts/{id} - delete a post. Returns nil (204).
195
+ # DELETE /posts/{id} - remove a post from OmniSocials (the live post stays on the platform). Returns nil (204).
188
196
  def delete(post_id)
189
197
  @client.request("DELETE", "/posts/#{post_id}")
190
198
  end
@@ -239,7 +247,8 @@ module OmniSocials
239
247
  hashtag_set_id:, hashtag_placement:, hashtag_platforms:,
240
248
  pinterest:, youtube:, instagram:, facebook:, linkedin:,
241
249
  linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
242
- threads:, google_business:, linkedin_poll:)
250
+ threads:, google_business:, linkedin_poll:,
251
+ approval_workflow_id: nil)
243
252
  Internal.drop_nil(
244
253
  {
245
254
  "content" => content,
@@ -272,7 +281,8 @@ module OmniSocials
272
281
  "mastodon" => mastodon,
273
282
  "threads" => threads,
274
283
  "google_business" => google_business,
275
- "linkedin_poll" => linkedin_poll
284
+ "linkedin_poll" => linkedin_poll,
285
+ "approval_workflow_id" => approval_workflow_id
276
286
  }
277
287
  )
278
288
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/omnisocials.rb CHANGED
@@ -20,6 +20,7 @@ require_relative "omnisocials/resources/posts"
20
20
  require_relative "omnisocials/resources/media"
21
21
  require_relative "omnisocials/resources/folders"
22
22
  require_relative "omnisocials/resources/hashtag_sets"
23
+ require_relative "omnisocials/resources/approval_workflows"
23
24
  require_relative "omnisocials/resources/accounts"
24
25
  require_relative "omnisocials/resources/analytics"
25
26
  require_relative "omnisocials/resources/audio"
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.5.0
4
+ version: 0.6.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-30 00:00:00.000000000 Z
11
+ date: 2026-09-17 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,
@@ -27,6 +27,7 @@ files:
27
27
  - lib/omnisocials/internal.rb
28
28
  - lib/omnisocials/resources/accounts.rb
29
29
  - lib/omnisocials/resources/analytics.rb
30
+ - lib/omnisocials/resources/approval_workflows.rb
30
31
  - lib/omnisocials/resources/audio.rb
31
32
  - lib/omnisocials/resources/folders.rb
32
33
  - lib/omnisocials/resources/hashtag_sets.rb