omnisocials 0.11.0 → 0.13.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: 473f213709ed52c13f70cd2b84c26b7d92c2355731dfc0fb676d3b0f3949fbac
4
- data.tar.gz: 423f50628c3c4dae0b98d3c486337a573cb32972af6cdf74cd41a8646899f162
3
+ metadata.gz: 712ed8026c6e6223f5d2bbcaaa0696cda2ed181ab8f4da6bd2b95fd2430bc84e
4
+ data.tar.gz: 2e1cf587a451e8f966800ddef4f74b1b8a0422e0df7fd7ffb078afdf5e1636eb
5
5
  SHA512:
6
- metadata.gz: 5a047afd2262cd53375d22fef8b7c79ed254fddf8e5057cdb4cdd909308821bc19c81b73cddf04ad736cecca998bf0ee0f974f6205d2e0bd4472547ee4464dfa
7
- data.tar.gz: 27c340cc9e7954eae71506ed026b10b1421130cb59c283cab8fdbac3016759b5e449c406367579acbc3cf4c4c1b70fb151750350d8e6bbb27fe330eaaf49cba3
6
+ metadata.gz: f45800a5a3a36c56e37df9085ee31c95182cfaec1f1a30224a65a82d538884f66c40bbd677b48a47236dd92dcc41e81db6e869c066d10f4b360debb566eb1d80
7
+ data.tar.gz: 48569b613c4247848a3638727b65f8ba71ac1a9e1f1051c12843f02e738771536058290909d6623a97b5c1dc09d92eacf9c5c12c917196b5f7467917178e320e
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OmniSocials Ruby SDK
2
2
 
3
- The official Ruby SDK for OmniSocials: post, schedule, reply and report on 12 social platforms (Instagram, Facebook, LinkedIn, X, TikTok, YouTube, Threads, Pinterest, Bluesky, Mastodon, Google Business and Telegram) from one API. Guide: [omnisocials.com/sdk/ruby](https://omnisocials.com/sdk/ruby)
3
+ The official Ruby SDK for OmniSocials: post, schedule, reply and report on 13 social platforms (Instagram, Facebook, LinkedIn, X, TikTok, YouTube, Threads, Pinterest, Bluesky, Mastodon, Google Business, Telegram and Discord) from one API. Guide: [omnisocials.com/sdk/ruby](https://omnisocials.com/sdk/ruby)
4
4
 
5
5
  Full API reference: [https://docs.omnisocials.com](https://docs.omnisocials.com)
6
6
 
@@ -111,7 +111,11 @@ events["data"].each do |event|
111
111
  end
112
112
  ```
113
113
 
114
- The events response is `{ "data", "counts", "pagination" }`, not the plain list: `counts` is the number of events per outcome (`sent`, `skipped`, `failed`, `pending`). An automation can also listen on a post published outside OmniSocials (`client.automations.create(instagram_media_id: ..., actions: ...)`), on the next post the workspace publishes (`trigger: { "scope" => "next_post" }`) or on every post (`"scope" => "any"`), and on DMs and story replies (`trigger: { "kinds" => ["dm", "story_reply"], ... }`).
114
+ The events response is `{ "data", "counts", "pagination" }`, not the plain list: `counts` is the number of events per outcome (`sent`, `skipped`, `failed`, `pending`, `queued`, `opted_out`). An automation can also listen on a post published outside OmniSocials (`client.automations.create(instagram_media_id: ..., actions: ...)`), on the next post the workspace publishes (`trigger: { "scope" => "next_post" }`) or on every post (`"scope" => "any"`), and on DMs and story replies (`trigger: { "kinds" => ["dm", "story_reply"], ... }`).
115
+
116
+ A Facebook Page works the same way, and the DM lands in Messenger: pass `platform: "facebook"` to `client.automations.create`, or `facebook_post_id: ...` for a Page post published outside OmniSocials. On a post, `comment_dm_automation: { "platforms" => ["instagram", "facebook"], ... }` makes one automation per channel with the same keywords and DM. Facebook has no story replies.
117
+
118
+ On X, pass `platform: "x"` with `trigger: { "kinds" => ["dm"], "keywords" => ["guide"] }`: the X account answers a keyword in a DM, once per person, and every answer ends with "Reply STOP to stop automatic messages." An active X automation needs X DMs on for the workspace (`409 x_dms_off`; `status: "paused"` always saves). An answer over the send cap waits in a queue (`queued`) for up to an hour.
115
119
 
116
120
  ### Per-media alt text
117
121
 
@@ -177,6 +181,30 @@ client.posts.create(
177
181
  )
178
182
  ```
179
183
 
184
+ ### Discord channels
185
+
186
+ `discord` is one Discord channel the workspace connected in OmniSocials; the post goes out through the channel's incoming webhook and shows the webhook's name and picture. The `discord` options: `silent`, `link_preview` (default on), `allow_mentions` (default off, so @everyone, @here, role and user mentions show as text and ping nobody) and `spoiler` (booleans), `thread_name` (the post title in a forum or media channel, 1 to 100 characters), up to 5 `buttons` (`{ text, url }`, one row, not with a poll) and a native `poll` (2 to 10 options of up to 55 characters, `duration_hours` 1 to 768, default 24, `multiple`; the post text is the question, max 300 characters, no media). Text up to 2,000 characters. Up to 10 images, GIFs and videos per message; a video or GIF over Discord's 20 MB attachment limit is sent as a link in the text. No Stories. Once sent, the post's `discord.result` carries the message id and the link. Discord gives webhooks no view counts: analytics carry the reactions and the poll votes.
187
+
188
+ ```ruby
189
+ client.posts.create(
190
+ content: "Patch 2.4 is out. Read the notes below.",
191
+ channels: ["discord"],
192
+ scheduled_at: "2026-11-02T09:00:00Z",
193
+ media_urls: ["https://example.com/patch-notes.png"],
194
+ discord: {
195
+ "silent" => true,
196
+ "buttons" => [{ "text" => "Patch notes", "url" => "https://example.com/patch-2-4" }]
197
+ }
198
+ )
199
+
200
+ # A native poll: the text is the question.
201
+ client.posts.create(
202
+ content: "Which mode do we test next?",
203
+ channels: ["discord"],
204
+ discord: { "poll" => { "options" => %w[Ranked Co-op Arena], "duration_hours" => 48 } }
205
+ )
206
+ ```
207
+
180
208
  ### Chained threads (X, Bluesky, Mastodon, Threads)
181
209
 
182
210
  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):
@@ -216,6 +244,8 @@ On update, passing `x: { "thread_parts" => nil }` clears the thread and reverts
216
244
 
217
245
  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.
218
246
 
247
+ Threads posts can carry a topic too: pass `threads: { "topic_tag" => "Build in public" }`. Unlike a #tag in the text, a topic can hold spaces. 1 to 50 characters, no period and no ampersand; a leading `#` is dropped. On a multi-post thread the topic is applied to part 1, and on update `threads: { "topic_tag" => nil }` removes it.
248
+
219
249
  ### X link posts use credits
220
250
 
221
251
  X bills API posts whose text contains a URL at a premium, and OmniSocials passes that fee through as prepaid credits (20 credits per URL-containing tweet; threads billed per part with a link). When a create targets X and the text contains a URL, the response Hash includes a top-level `"warnings"` array (a sibling of `"data"`):
@@ -515,9 +545,15 @@ message_id = messages["data"][0]["id"]
515
545
  client.inbox.hide(message_id) # hide
516
546
  client.inbox.hide(message_id, hide: false) # unhide
517
547
  client.inbox.delete_message(message_id)
548
+
549
+ # Like a comment as the Page or account (Facebook, LinkedIn Page, TikTok).
550
+ # The returned message carries its "liked" flag.
551
+ liked = client.inbox.like(message_id) # like
552
+ client.inbox.like(message_id, like: false) # unlike
553
+ puts liked["data"]["liked"] # true
518
554
  ```
519
555
 
520
- `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`. The Threads inbox needs a Threads connection with the reply permission (a 401 `reauth_required` means reconnect Threads; an account connected before 2026-09-14 needs this once). 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"`).
556
+ `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. `liked` is `true`/`false` on Facebook, LinkedIn Page, and TikTok comments (whether the connected account liked it; on Facebook and LinkedIn it reflects likes made through OmniSocials, TikTok also syncs likes made in the TikTok app) and `nil` where the platform has no like through OmniSocials (DMs, Instagram, YouTube, X, Threads). 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`. The Threads inbox needs a Threads connection with the reply permission (a 401 `reauth_required` means reconnect Threads; an account connected before 2026-09-14 needs this once). 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"`).
521
557
 
522
558
  `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. DMs that can still be answered come first, then Instagram/Facebook DMs whose 24-hour window has closed (`reply_window["open"]` is `false`: answer those from the native app or mark them read), then comments and mentions, oldest first. 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. Always pass `message_id: message["id"]` to `reply` on comment threads: every comment on a post shares one conversation, and without it the reply goes under the newest comment on the post. 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.
523
559
 
@@ -555,7 +591,7 @@ rescue OmniSocials::APIError => e
555
591
  when "insufficient_credits"
556
592
  puts "Not enough credits to send this reply (need 2)."
557
593
  when "x_inbox_suspended"
558
- puts "X inbox is suspended - top up and re-enable it in the dashboard."
594
+ puts "X inbox is suspended until a top-up."
559
595
  else
560
596
  raise
561
597
  end
@@ -5,9 +5,15 @@ module OmniSocials
5
5
  # Automations resource: comment-to-DM automations (list, get, create,
6
6
  # update, delete, events). An automation answers a keyword in an
7
7
  # Instagram comment (or a DM, or a story reply) with a DM, once per
8
- # person, with an optional public reply under the comment. The scopes
9
- # are opt-in on the API key: automations:read for list / get /
10
- # list_events, automations:write for create / update / delete (and for
8
+ # person, with an optional public reply under the comment. With
9
+ # platform: "facebook" it runs on a Facebook Page instead, and the DM
10
+ # lands in Messenger (a Page has no story replies). With platform: "x"
11
+ # it answers a keyword in a DM to the X account ("dm" kind only, no
12
+ # post, no public reply) while X DMs are on for the workspace; every X
13
+ # answer ends with "Reply STOP to stop automatic messages." and a
14
+ # person who replies STOP gets no automatic DM again. The scopes are
15
+ # opt-in on the API key: automations:read for list / get / list_events,
16
+ # automations:write for create / update / delete (and for
11
17
  # comment_dm_automation on posts).
12
18
  class Automations
13
19
  def initialize(client)
@@ -15,13 +21,14 @@ module OmniSocials
15
21
  end
16
22
 
17
23
  # GET /automations - list the workspace's comment-to-DM automations,
18
- # optionally by status ("active", "paused", "completed") or by the
19
- # OmniSocials post_id they are bound to. Requires the automations:read
20
- # scope (opt-in on the API key).
21
- def list(status: nil, post_id: nil)
24
+ # optionally by status ("active", "paused", "completed"), by platform
25
+ # ("instagram", "facebook", "x") or by the OmniSocials post_id they are
26
+ # bound to. Requires the automations:read scope (opt-in on the API
27
+ # key).
28
+ def list(status: nil, post_id: nil, platform: nil)
22
29
  @client.request(
23
30
  "GET", "/automations",
24
- query: { "status" => status, "post_id" => post_id }
31
+ query: { "status" => status, "platform" => platform, "post_id" => post_id }
25
32
  )
26
33
  end
27
34
 
@@ -35,10 +42,21 @@ module OmniSocials
35
42
  #
36
43
  # A keyword in an Instagram comment (or a DM, or a story reply) is
37
44
  # answered with a DM, once per person, with an optional public reply
38
- # under the comment. Bind it to one post with post_id (an OmniSocials
39
- # post that goes to Instagram, scheduled or live; one automation per
40
- # post) or instagram_media_id (a post published outside OmniSocials),
41
- # not both; a trigger scope of "next_post" or "any" needs neither.
45
+ # under the comment. platform is "instagram" (default) or "facebook":
46
+ # on a Facebook Page a keyword in a comment (or a Messenger message to
47
+ # the Page) is answered in Messenger, and a Page has no story replies.
48
+ # platform: "x" answers a keyword in a DM to the X account: trigger
49
+ # kinds must be ["dm"] (X allows an automated DM only after the person
50
+ # wrote first), with no post and no public reply; an active X
51
+ # automation needs X DMs on for the workspace (409 x_dms_off; status
52
+ # "paused" always saves), and every X answer ends with "Reply STOP to
53
+ # stop automatic messages.". Bind it to one post with post_id (an OmniSocials post that goes to
54
+ # the channel, scheduled or live; one automation per post and
55
+ # platform), instagram_media_id (an Instagram post published outside
56
+ # OmniSocials) or facebook_post_id (a Page post published outside
57
+ # OmniSocials, as "{page-id}_{post-id}" or the post id alone; implies
58
+ # "facebook"), at most one; a trigger scope of "next_post" or "any"
59
+ # needs none.
42
60
  #
43
61
  # `actions` is required: { "dm" => { "text" => "..." } } (max 1000
44
62
  # characters) plus an optional "public_reply" => { "enabled" => true,
@@ -49,20 +67,27 @@ module OmniSocials
49
67
  # characters, required unless match is "any"; match is "word",
50
68
  # "contains" or "any". `limits` is { "max_sends" => 500, "stop_at" =>
51
69
  # "2026-12-31T00:00:00Z" } (both optional). `status` is "active"
52
- # (default) or "paused". Instagram rules: one private reply per
53
- # comment, within 7 days; OmniSocials caps sends at 180 DMs an hour per
54
- # account.
70
+ # (default) or "paused". Meta rules, the same on Instagram and
71
+ # Facebook: one private reply per comment, within 7 days; OmniSocials
72
+ # caps sends at 180 DMs an hour per account and platform, and on X at
73
+ # 15 per 15 minutes and 450 a day; an answer over a cap waits in a
74
+ # queue ("queued") for up to an hour.
55
75
  #
56
76
  # Requires the automations:write scope (opt-in on the API key).
57
77
  # Errors: 400 validation_error, 400 post_not_instagram, 400
58
- # post_required, 404 post_not_found, 404 instagram_media_not_found,
59
- # 409 instagram_not_connected, 409 instagram_needs_reauth, 409
60
- # automation_exists (the error body also carries automation_id).
78
+ # post_not_facebook, 400 post_required, 404 post_not_found, 404
79
+ # instagram_media_not_found, 404 facebook_post_not_found, 409
80
+ # instagram_not_connected, 409 instagram_needs_reauth, 409
81
+ # facebook_not_connected, 409 facebook_needs_reauth, 409
82
+ # x_not_connected, 409 x_dms_off, 409 automation_exists (the error
83
+ # body also carries automation_id).
61
84
  def create(actions:, trigger: nil, limits: nil, name: nil, post_id: nil,
62
- instagram_media_id: nil, status: nil)
85
+ instagram_media_id: nil, status: nil, platform: nil,
86
+ facebook_post_id: nil)
63
87
  body = automation_body(
64
88
  name: name, post_id: post_id, instagram_media_id: instagram_media_id,
65
- trigger: trigger, actions: actions, limits: limits, status: status
89
+ trigger: trigger, actions: actions, limits: limits, status: status,
90
+ platform: platform, facebook_post_id: facebook_post_id
66
91
  )
67
92
  @client.request("POST", "/automations", json: body)
68
93
  end
@@ -76,15 +101,20 @@ module OmniSocials
76
101
  # "public_reply" => { "enabled" => false } or { "variations" => [] }
77
102
  # turns the public reply off; limits: { "max_sends" => nil } removes
78
103
  # that limit. status: "paused" pauses and "active" resumes; another
79
- # post_id / instagram_media_id moves the automation to that post. A
104
+ # post_id / instagram_media_id / facebook_post_id moves the automation
105
+ # to that post. The platform cannot change: a platform other than the
106
+ # automation's is a 400 platform_immutable. status: "active" on an X
107
+ # automation needs X DMs on for the workspace (409 x_dms_off). A
80
108
  # "completed" automation starts again when its limits or trigger
81
- # change. Requires the automations:write scope. 400 no_fields when
82
- # nothing is passed.
109
+ # change. Requires the
110
+ # automations:write scope. 400 no_fields when nothing is passed.
83
111
  def update(automation_id, name: nil, post_id: nil, instagram_media_id: nil,
84
- trigger: nil, actions: nil, limits: nil, status: nil)
112
+ trigger: nil, actions: nil, limits: nil, status: nil,
113
+ platform: nil, facebook_post_id: nil)
85
114
  body = automation_body(
86
115
  name: name, post_id: post_id, instagram_media_id: instagram_media_id,
87
- trigger: trigger, actions: actions, limits: limits, status: status
116
+ trigger: trigger, actions: actions, limits: limits, status: status,
117
+ platform: platform, facebook_post_id: facebook_post_id
88
118
  )
89
119
  @client.request("PATCH", "/automations/#{automation_id}", json: body)
90
120
  end
@@ -97,15 +127,20 @@ module OmniSocials
97
127
 
98
128
  # GET /automations/{id}/events - the automation's activity: one event
99
129
  # per person it answered, skipped or failed on. outcome filters on
100
- # "sent", "skipped", "failed" or "pending"; limit is 1..100 (default
101
- # 25), offset skips. Requires the automations:read scope.
130
+ # "sent", "skipped", "failed", "pending", "queued" (waits for a free
131
+ # slot under the send cap, out within the hour) or "opted_out" (the
132
+ # person replied STOP on X); limit is 1..100 (default 25), offset
133
+ # skips. Requires the automations:read scope.
102
134
  #
103
135
  # The response is not the plain list envelope: { "data" => [...],
104
136
  # "counts" => { "sent" => 12, "skipped" => 3 }, "pagination" => {...} },
105
137
  # where counts is the number of events per outcome (an outcome with no
106
138
  # events is left out). Each event carries kind ("comment", "dm",
107
- # "story_reply"), outcome, reason / reason_label, person (id,
108
- # username), trigger_id, trigger_text, message (the DM sent),
139
+ # "story_reply"), outcome, reason / reason_label (X adds the skip
140
+ # reasons "dms_closed", "no_credits", "opted_out" and
141
+ # "automation_off"), person (id,
142
+ # username; on Facebook the person's name, since Facebook has no
143
+ # @handle), trigger_id, trigger_text, message (the DM sent),
109
144
  # public_reply, platform_message_id, created_at and sent_at.
110
145
  def list_events(automation_id, outcome: nil, limit: nil, offset: nil)
111
146
  @client.request(
@@ -117,12 +152,15 @@ module OmniSocials
117
152
  private
118
153
 
119
154
  def automation_body(name:, post_id:, instagram_media_id:, trigger:,
120
- actions:, limits:, status:)
155
+ actions:, limits:, status:, platform: nil,
156
+ facebook_post_id: nil)
121
157
  Internal.drop_nil(
122
158
  {
159
+ "platform" => platform,
123
160
  "name" => name,
124
161
  "post_id" => post_id,
125
162
  "instagram_media_id" => instagram_media_id,
163
+ "facebook_post_id" => facebook_post_id,
126
164
  "trigger" => trigger,
127
165
  "actions" => actions,
128
166
  "limits" => limits,
@@ -167,6 +167,32 @@ module OmniSocials
167
167
  )
168
168
  end
169
169
 
170
+ # POST /inbox/messages/{id}/like - like or unlike a comment someone left
171
+ # on one of the user's posts, as the connected Page or account.
172
+ # Facebook Page, LinkedIn Page and TikTok comments (incoming comments
173
+ # only). Pass like: false to remove the like. Facebook allows only Like
174
+ # (no Love or other reactions). Liking a comment that is already liked,
175
+ # or unliking one that is not, succeeds. Returns the comment after the
176
+ # change with its "liked" flag set: true when the account liked it,
177
+ # false when not, nil where the platform has no like through
178
+ # OmniSocials (DMs, Instagram, YouTube, X, Threads). On Facebook and
179
+ # LinkedIn "liked" reflects likes made through OmniSocials; TikTok also
180
+ # syncs likes made in the TikTok app. Requires the inbox:write scope.
181
+ #
182
+ # Errors: 400 "unsupported_platform" (not an incoming Facebook, LinkedIn
183
+ # or TikTok comment), 401 "reauth_required" (the TikTok comments
184
+ # authorization is missing or expired, or the LinkedIn Page connection expired), 403 "reconnect_required" (the
185
+ # account was connected without the permission to like comments;
186
+ # reconnect it in the dashboard), 404 "not_found" (message not in this
187
+ # workspace) or "account_not_connected", 502 "platform_error" (the
188
+ # platform rejected the call).
189
+ def like(message_id, like: true)
190
+ @client.request(
191
+ "POST", "/inbox/messages/#{encode_id(message_id)}/like",
192
+ json: { "like" => like }
193
+ )
194
+ end
195
+
170
196
  # DELETE /inbox/messages/{id} - delete a comment someone left on one of
171
197
  # the user's posts, on the platform and from the inbox. Facebook,
172
198
  # Instagram and TikTok comments only: YouTube's API does not let a
@@ -218,9 +244,9 @@ module OmniSocials
218
244
  # { "conversation", "message", "messages", "reply_window" }, or nil
219
245
  # when nothing is waiting. "message" is the unanswered incoming item
220
246
  # itself (the customer's latest DM, or the specific comment): its "id"
221
- # is what `hide` and `delete_message` take and the message_id: to pass
222
- # to `reply` on comment threads, its "conversation_id" is what `reply`
223
- # takes. "messages" is the conversation so far, oldest first (the most
247
+ # is what `hide`, `like` and `delete_message` take and the message_id:
248
+ # to pass to `reply` on comment threads, its "conversation_id" is what
249
+ # `reply` takes. "messages" is the conversation so far, oldest first (the most
224
250
  # recent 50 messages for long DM threads). "reply_window" is
225
251
  # { "open" => Boolean, "closes_at" => String|nil }: "open" is false
226
252
  # only for an Instagram/Facebook DM past its 24-hour window, which
@@ -106,6 +106,11 @@ module OmniSocials
106
106
  # 400 validation_error asking you to reconnect Threads when the
107
107
  # connection lacks the threads_location_tagging permission).
108
108
  #
109
+ # Threads posts can carry a topic: threads: { "topic_tag" => "Build in
110
+ # public" } (1 to 50 characters, no period and no ampersand; a topic can
111
+ # hold spaces). On a multi-post thread it goes on part 1; on update
112
+ # "topic_tag" => nil removes it.
113
+ #
109
114
  # Pinterest posts can tag products: pass
110
115
  # pinterest: { "board_id" => "...", "product_tags" => [...] } with up
111
116
  # to 24 product Pins of the connected Pinterest account, each as a Pin
@@ -129,19 +134,41 @@ module OmniSocials
129
134
  # stored options. Once sent, the Post's "telegram" block carries
130
135
  # "result" (message ids and links).
131
136
  #
137
+ # discord holds the Discord options (one Discord channel, posted
138
+ # through its incoming webhook with the webhook's name and picture):
139
+ # "silent", "link_preview" (default on), "allow_mentions" (default off:
140
+ # @everyone, @here, role and user mentions show as text and ping
141
+ # nobody), "spoiler" (booleans), "thread_name" (the post title in a
142
+ # forum or media channel, 1 to 100 characters; on a text channel the
143
+ # post goes out without it), up to 5 "buttons" ({ "text" => 1 to 80 characters, "url" =>
144
+ # https:// or http:// }, one row, not with a poll) and a native "poll"
145
+ # ({ "options" => 2 to 10 of up to 55 characters, "duration_hours" =>
146
+ # 1 to 768 (default 24), "multiple" => true }; the post text is the
147
+ # question, max 300 characters, no media and no buttons). Text up to
148
+ # 2,000 characters; up to 10 images, GIFs and videos per message, a
149
+ # video or GIF over 20 MB is sent as a link in the text. No Stories.
150
+ # On #update every key merges into the stored options, a key set to
151
+ # nil removes it and discord: nil removes every option. Once sent, the
152
+ # Post's "discord" block carries "result" (message id and link).
153
+ #
132
154
  # comment_dm_automation binds a comment-to-DM automation to the post
133
- # (Instagram must be in channels): a keyword in a comment is answered
134
- # with a DM, once per person, with an optional public reply under the
135
- # comment. Shape: { "trigger" => { "keywords" => ["guide"], "match" =>
136
- # "word" }, "actions" => { "dm" => { "text" => "..." }, "public_reply"
137
- # => { "enabled" => true, "variations" => [...] } }, "limits" =>
138
- # { "max_sends" => 500 } } (see automations.create for the fields).
139
- # Needs the automations:write scope on the key (403
140
- # insufficient_scope, checked before anything else); a bad Hash is a
141
- # 400 validation_error whose message starts with
155
+ # (Instagram or Facebook must be in channels): a keyword in a comment
156
+ # is answered with a DM, once per person, with an optional public
157
+ # reply under the comment. Shape: { "trigger" => { "keywords" =>
158
+ # ["guide"], "match" => "word" }, "actions" => { "dm" => { "text" =>
159
+ # "..." }, "public_reply" => { "enabled" => true, "variations" =>
160
+ # [...] } }, "limits" => { "max_sends" => 500 }, "platforms" =>
161
+ # ["instagram", "facebook"] } (see automations.create for the
162
+ # fields). "platforms" makes one automation per listed channel; each
163
+ # must be a channel of the post. Without it: Instagram when the post
164
+ # goes there, else Facebook. Needs the automations:write scope on the
165
+ # key (403 insufficient_scope, checked before anything else); a bad
166
+ # Hash is a 400 validation_error whose message starts with
142
167
  # "comment_dm_automation: ". The response then carries
143
- # data["comment_dm_automation"] (the automation, with "post" nil); it
144
- # starts listening once the post is live on Instagram.
168
+ # data["comment_dm_automation"] (the first automation, with "post"
169
+ # nil, plus "platforms" and "automations", one { "id", "platform",
170
+ # "status" } per channel); each starts listening once the post is
171
+ # live on its channel.
145
172
  def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
146
173
  media_urls: nil, type: nil, source: nil, link_url: nil,
147
174
  link_title: nil, link_description: nil, link_thumbnail_url: nil,
@@ -151,7 +178,7 @@ module OmniSocials
151
178
  instagram: nil, facebook: nil, linkedin: nil,
152
179
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
153
180
  mastodon: nil, threads: nil, google_business: nil,
154
- telegram: nil, linkedin_poll: nil,
181
+ telegram: nil, discord: nil, linkedin_poll: nil,
155
182
  approval_workflow_id: nil, draft: nil, media_fit: nil,
156
183
  comment_dm_automation: nil)
157
184
  body = create_body(
@@ -167,7 +194,7 @@ module OmniSocials
167
194
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
168
195
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
169
196
  google_business: google_business, telegram: telegram,
170
- linkedin_poll: linkedin_poll,
197
+ discord: discord, linkedin_poll: linkedin_poll,
171
198
  approval_workflow_id: approval_workflow_id, draft: draft,
172
199
  media_fit: media_fit, comment_dm_automation: comment_dm_automation
173
200
  )
@@ -190,7 +217,8 @@ module OmniSocials
190
217
  facebook: nil, linkedin: nil, linkedin_page: nil,
191
218
  tiktok: nil, x: nil, bluesky: nil, mastodon: nil,
192
219
  threads: nil, google_business: nil,
193
- telegram: nil, linkedin_poll: nil, media_fit: nil,
220
+ telegram: nil, discord: nil, linkedin_poll: nil,
221
+ media_fit: nil,
194
222
  comment_dm_automation: nil)
195
223
  body = create_body(
196
224
  content: content, channels: channels, scheduled_at: nil,
@@ -205,7 +233,7 @@ module OmniSocials
205
233
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
206
234
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
207
235
  google_business: google_business, telegram: telegram,
208
- linkedin_poll: linkedin_poll,
236
+ discord: discord, linkedin_poll: linkedin_poll,
209
237
  media_fit: media_fit, comment_dm_automation: comment_dm_automation
210
238
  )
211
239
  @client.request("POST", "/posts/create-and-publish", json: body)
@@ -240,16 +268,21 @@ module OmniSocials
240
268
  # can also refuse an update to a scheduled X link post.
241
269
  #
242
270
  # comment_dm_automation (same shape as on #create) as a Hash replaces
243
- # the post's comment-to-DM automation, an explicit nil removes it, and
244
- # leaving it out keeps it. Needs the automations:write scope.
271
+ # the post's comment-to-DM automations (a channel no longer in
272
+ # "platforms" loses its automation), an explicit nil removes them all,
273
+ # and leaving it out keeps them. Needs the automations:write scope.
274
+ #
275
+ # discord merges into the stored Discord options: a key set to nil
276
+ # removes that option (e.g. discord: { "poll" => nil }), an explicit
277
+ # discord: nil removes every option, and leaving it out keeps them.
245
278
  def update(post_id, content: nil, scheduled_at: nil, channels: nil,
246
279
  media_ids: nil, media_urls: nil, type: nil, location_id: nil,
247
280
  collaborators: nil, user_tags: nil, pinterest: nil,
248
281
  youtube: nil, instagram: nil, facebook: nil, linkedin: nil,
249
282
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
250
283
  mastodon: nil, threads: nil, google_business: nil,
251
- telegram: nil, linkedin_poll: nil, draft: nil,
252
- media_fit: nil,
284
+ telegram: nil, discord: OmniSocials::NOT_GIVEN,
285
+ linkedin_poll: nil, draft: nil, media_fit: nil,
253
286
  comment_dm_automation: OmniSocials::NOT_GIVEN)
254
287
  body = Internal.drop_nil(
255
288
  {
@@ -283,6 +316,8 @@ module OmniSocials
283
316
  # An explicit nil removes the post's automation, so it must survive
284
317
  # drop_nil; NOT_GIVEN (the default) leaves it untouched.
285
318
  body["comment_dm_automation"] = comment_dm_automation unless comment_dm_automation.is_a?(NotGiven)
319
+ # Same for discord: an explicit nil removes every Discord option.
320
+ body["discord"] = discord unless discord.is_a?(NotGiven)
286
321
  @client.request("PATCH", "/posts/#{post_id}", json: body)
287
322
  end
288
323
 
@@ -358,7 +393,8 @@ module OmniSocials
358
393
  pinterest:, youtube:, instagram:, facebook:, linkedin:,
359
394
  linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
360
395
  threads:, google_business:, linkedin_poll:,
361
- telegram: nil, approval_workflow_id: nil, draft: nil,
396
+ telegram: nil, discord: nil, approval_workflow_id: nil,
397
+ draft: nil,
362
398
  media_fit: nil, comment_dm_automation: nil)
363
399
  Internal.drop_nil(
364
400
  {
@@ -393,6 +429,7 @@ module OmniSocials
393
429
  "threads" => threads,
394
430
  "google_business" => google_business,
395
431
  "telegram" => telegram,
432
+ "discord" => discord,
396
433
  "linkedin_poll" => linkedin_poll,
397
434
  "approval_workflow_id" => approval_workflow_id,
398
435
  "draft" => draft,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.11.0"
4
+ VERSION = "0.13.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.11.0
4
+ version: 0.13.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-10-21 00:00:00.000000000 Z
11
+ date: 2026-10-27 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,