omnisocials 0.9.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 +4 -4
- data/README.md +117 -5
- data/lib/omnisocials/client.rb +4 -3
- data/lib/omnisocials/resources/analytics.rb +3 -1
- data/lib/omnisocials/resources/automations.rb +173 -0
- data/lib/omnisocials/resources/inbox.rb +29 -3
- data/lib/omnisocials/resources/media.rb +12 -1
- data/lib/omnisocials/resources/posts.rb +134 -11
- data/lib/omnisocials/version.rb +1 -1
- data/lib/omnisocials.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 712ed8026c6e6223f5d2bbcaaa0696cda2ed181ab8f4da6bd2b95fd2430bc84e
|
|
4
|
+
data.tar.gz: 2e1cf587a451e8f966800ddef4f74b1b8a0422e0df7fd7ffb078afdf5e1636eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
|
@@ -30,7 +30,7 @@ puts post["data"]["id"]
|
|
|
30
30
|
|
|
31
31
|
## Authentication
|
|
32
32
|
|
|
33
|
-
Create an API key in the OmniSocials app under **Settings -> API Keys**. Keys
|
|
33
|
+
Create an API key in the OmniSocials app under **Settings -> API Keys**. Keys start with `omsk_live_` and are bound to one workspace. There are no test keys and no test mode: every key works on your real connected accounts. To try things safely, leave out `scheduled_at` so the post stays a draft (a draft never publishes on its own), or use a separate workspace with test accounts.
|
|
34
34
|
|
|
35
35
|
Pass the key explicitly or set the `OMNISOCIALS_API_KEY` environment variable:
|
|
36
36
|
|
|
@@ -65,6 +65,58 @@ post = client.posts.create_and_publish(
|
|
|
65
65
|
)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Planned drafts
|
|
69
|
+
|
|
70
|
+
Pass `draft: true` with `scheduled_at` to store the post as a draft that keeps its time. It shows on the dashboard calendar at that slot with a dashed outline and the scheduler never publishes it: a person schedules it from the composer, or you do with `update` and `draft: false`. Use it when an agent proposes a slot that a human should confirm. `draft: true` is not allowed with `publish_now` or `approval_workflow_id` (`400 validation_error`). On `update`, `draft: false` without `scheduled_at` is a `400 validation_error` when the draft has no time or its time is in the past (pass a new `scheduled_at` then), and `draft: true` on a scheduled post unschedules it while keeping the time.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
proposal = client.posts.create(
|
|
74
|
+
content: "Proposed for Friday morning",
|
|
75
|
+
channels: ["instagram", "linkedin"],
|
|
76
|
+
scheduled_at: "2026-08-01T09:00:00Z",
|
|
77
|
+
draft: true
|
|
78
|
+
)
|
|
79
|
+
puts proposal["data"]["status"], proposal["data"]["schedule_at"] # draft, the time is kept
|
|
80
|
+
|
|
81
|
+
# The human agrees: schedule it at the time it already carries
|
|
82
|
+
client.posts.update(proposal["data"]["id"], draft: false)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Comment-to-DM automations
|
|
86
|
+
|
|
87
|
+
Answer a keyword in an Instagram comment with a DM, once per person, with an optional public reply under the comment. Pass `comment_dm_automation:` on `create` (Instagram must be in `channels`) and the automation is bound to that post; it starts listening once the post is live. Needs the opt-in `automations:write` scope on the key. `client.automations` manages the automations themselves (`list`, `get`, `create`, `update`, `delete`) and reads their activity (`list_events`, `automations:read`). On `update`, `comment_dm_automation:` as a Hash replaces the post's automation and an explicit `nil` removes it. Instagram rules: one private reply per comment, within 7 days; OmniSocials caps sends at 180 DMs an hour per account.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
post = client.posts.create(
|
|
91
|
+
content: "Comment GUIDE and I will send you the link",
|
|
92
|
+
channels: ["instagram"],
|
|
93
|
+
scheduled_at: "2026-08-01T09:00:00Z",
|
|
94
|
+
media_urls: ["https://example.com/guide-cover.jpg"],
|
|
95
|
+
comment_dm_automation: {
|
|
96
|
+
"trigger" => { "keywords" => ["guide"], "match" => "word" },
|
|
97
|
+
"actions" => {
|
|
98
|
+
"dm" => { "text" => "Here is the guide: https://example.com/guide" },
|
|
99
|
+
"public_reply" => { "enabled" => true, "variations" => ["Sent you a DM!", "Check your inbox"] }
|
|
100
|
+
},
|
|
101
|
+
"limits" => { "max_sends" => 500 }
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
automation_id = post["data"]["comment_dm_automation"]["id"]
|
|
105
|
+
|
|
106
|
+
# Later: who was answered, and why someone was skipped
|
|
107
|
+
events = client.automations.list_events(automation_id, outcome: "sent")
|
|
108
|
+
puts events["counts"] # { "sent" => 12, "skipped" => 3 }
|
|
109
|
+
events["data"].each do |event|
|
|
110
|
+
puts [event["person"]["username"], event["outcome"], event["reason_label"]].join(" ")
|
|
111
|
+
end
|
|
112
|
+
```
|
|
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`, `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.
|
|
119
|
+
|
|
68
120
|
### Per-media alt text
|
|
69
121
|
|
|
70
122
|
Every `media_urls` / `media_ids` entry accepts either a plain String or a Hash with an `alt` accessibility description (max 1500 chars). Alt text is delivered to Mastodon (media description), Bluesky (embed alt), X (photos and GIFs), Pinterest (pin alt text), Instagram (images), and LinkedIn (images). Strings and Hashes can be mixed, and the same shape works in per-platform Hashes and `thread_parts` media.
|
|
@@ -103,6 +155,56 @@ post = client.posts.create(
|
|
|
103
155
|
|
|
104
156
|
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
157
|
|
|
158
|
+
### Telegram channels and groups
|
|
159
|
+
|
|
160
|
+
`telegram` is a channel or group the workspace connected in OmniSocials; the OmniSocials bot posts to it. The `telegram` options: `silent`, `link_preview`, `pin`, `protect_content`, `spoiler` and `caption_above_media` (booleans), up to 3 `buttons` (`{ text, url }`, not on an album or a poll), a native `poll` (2 to 12 options of up to 100 characters, `open_period` 5 to 2628000 seconds, `multiple`; the post text is the question, max 300 characters, no media) and `first_comment` (a reply under the post). Text up to 4,096 characters; with media, text over 1,024 characters is sent as a reply under the media. Albums hold up to 10 photos and videos, files up to 50 MB, no Stories. Once sent, the post's `telegram.result` carries the message ids and the link. Telegram gives bots no view counts: analytics carry the reactions and the poll votes.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
client.posts.create(
|
|
164
|
+
content: "The new collection is live. Tap the button to shop.",
|
|
165
|
+
channels: ["telegram"],
|
|
166
|
+
scheduled_at: "2026-11-02T09:00:00Z",
|
|
167
|
+
media_urls: ["https://example.com/collection.jpg"],
|
|
168
|
+
telegram: {
|
|
169
|
+
"silent" => true,
|
|
170
|
+
"pin" => true,
|
|
171
|
+
"buttons" => [{ "text" => "Shop now", "url" => "https://example.com/shop" }],
|
|
172
|
+
"first_comment" => "Questions? Reply to this message."
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# A native poll: the text is the question.
|
|
177
|
+
client.posts.create(
|
|
178
|
+
content: "Which day suits the live Q&A?",
|
|
179
|
+
channels: ["telegram"],
|
|
180
|
+
telegram: { "poll" => { "options" => %w[Tuesday Thursday Saturday], "open_period" => 86_400 } }
|
|
181
|
+
)
|
|
182
|
+
```
|
|
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
|
+
|
|
106
208
|
### Chained threads (X, Bluesky, Mastodon, Threads)
|
|
107
209
|
|
|
108
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):
|
|
@@ -142,6 +244,8 @@ On update, passing `x: { "thread_parts" => nil }` clears the thread and reverts
|
|
|
142
244
|
|
|
143
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.
|
|
144
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
|
+
|
|
145
249
|
### X link posts use credits
|
|
146
250
|
|
|
147
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"`):
|
|
@@ -350,9 +454,11 @@ accounts = client.analytics.accounts(platform: "instagram")
|
|
|
350
454
|
|
|
351
455
|
### Best times to post
|
|
352
456
|
|
|
457
|
+
The response is not wrapped in `data`. `platform`, `timezone`, `basis`, `grid` and `recommendations` sit at the top level.
|
|
458
|
+
|
|
353
459
|
```ruby
|
|
354
460
|
best = client.analytics.best_times(platform: "instagram", timezone: "Europe/Amsterdam")
|
|
355
|
-
best["
|
|
461
|
+
best["recommendations"].each { |slot| puts "#{slot["day"]} #{slot["time"]} #{slot["score"]}" }
|
|
356
462
|
```
|
|
357
463
|
|
|
358
464
|
## Locations (Instagram and Threads place tagging)
|
|
@@ -439,9 +545,15 @@ message_id = messages["data"][0]["id"]
|
|
|
439
545
|
client.inbox.hide(message_id) # hide
|
|
440
546
|
client.inbox.hide(message_id, hide: false) # unhide
|
|
441
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
|
|
442
554
|
```
|
|
443
555
|
|
|
444
|
-
`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"`).
|
|
445
557
|
|
|
446
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.
|
|
447
559
|
|
|
@@ -479,7 +591,7 @@ rescue OmniSocials::APIError => e
|
|
|
479
591
|
when "insufficient_credits"
|
|
480
592
|
puts "Not enough credits to send this reply (need 2)."
|
|
481
593
|
when "x_inbox_suspended"
|
|
482
|
-
puts "X inbox is suspended
|
|
594
|
+
puts "X inbox is suspended until a top-up."
|
|
483
595
|
else
|
|
484
596
|
raise
|
|
485
597
|
end
|
data/lib/omnisocials/client.rb
CHANGED
|
@@ -60,10 +60,10 @@ module OmniSocials
|
|
|
60
60
|
|
|
61
61
|
attr_reader :api_key, :base_url, :timeout, :max_retries,
|
|
62
62
|
:posts, :media, :folders, :hashtag_sets, :approval_workflows,
|
|
63
|
-
:accounts, :analytics, :audio, :locations, :pinterest,
|
|
64
|
-
:webhooks
|
|
63
|
+
:accounts, :analytics, :audio, :locations, :pinterest,
|
|
64
|
+
:automations, :inbox, :webhooks
|
|
65
65
|
|
|
66
|
-
# api_key - API key (omsk_live_*
|
|
66
|
+
# api_key - API key (omsk_live_*). Falls back to the
|
|
67
67
|
# OMNISOCIALS_API_KEY environment variable. Raises
|
|
68
68
|
# OmniSocials::AuthenticationError when neither is set.
|
|
69
69
|
# base_url - API base URL (default https://api.omnisocials.com/v1).
|
|
@@ -96,6 +96,7 @@ module OmniSocials
|
|
|
96
96
|
@audio = Resources::Audio.new(self)
|
|
97
97
|
@locations = Resources::Locations.new(self)
|
|
98
98
|
@pinterest = Resources::Pinterest.new(self)
|
|
99
|
+
@automations = Resources::Automations.new(self)
|
|
99
100
|
@inbox = Resources::Inbox.new(self)
|
|
100
101
|
@webhooks = Resources::Webhooks.new(self)
|
|
101
102
|
end
|
|
@@ -40,7 +40,9 @@ module OmniSocials
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# GET /analytics/best-times - best times to post for a platform,
|
|
43
|
-
# derived from the workspace's own engagement history.
|
|
43
|
+
# derived from the workspace's own engagement history. The response has
|
|
44
|
+
# no "data" wrapper: "platform", "timezone", "basis", "grid" and
|
|
45
|
+
# "recommendations" sit at the top level.
|
|
44
46
|
def best_times(platform:, timezone: nil)
|
|
45
47
|
@client.request(
|
|
46
48
|
"GET", "/analytics/best-times",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniSocials
|
|
4
|
+
module Resources
|
|
5
|
+
# Automations resource: comment-to-DM automations (list, get, create,
|
|
6
|
+
# update, delete, events). An automation answers a keyword in an
|
|
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. 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
|
|
17
|
+
# comment_dm_automation on posts).
|
|
18
|
+
class Automations
|
|
19
|
+
def initialize(client)
|
|
20
|
+
@client = client
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# GET /automations - list the workspace's comment-to-DM automations,
|
|
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)
|
|
29
|
+
@client.request(
|
|
30
|
+
"GET", "/automations",
|
|
31
|
+
query: { "status" => status, "platform" => platform, "post_id" => post_id }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# GET /automations/{id} - fetch a single automation. Requires the
|
|
36
|
+
# automations:read scope (opt-in on the API key).
|
|
37
|
+
def get(automation_id)
|
|
38
|
+
@client.request("GET", "/automations/#{automation_id}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# POST /automations - create a comment-to-DM automation.
|
|
42
|
+
#
|
|
43
|
+
# A keyword in an Instagram comment (or a DM, or a story reply) is
|
|
44
|
+
# answered with a DM, once per person, with an optional public reply
|
|
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.
|
|
60
|
+
#
|
|
61
|
+
# `actions` is required: { "dm" => { "text" => "..." } } (max 1000
|
|
62
|
+
# characters) plus an optional "public_reply" => { "enabled" => true,
|
|
63
|
+
# "variations" => [...] } (comment kind only, up to 10 of 300
|
|
64
|
+
# characters). `trigger` is { "kinds" => ["comment"], "scope" =>
|
|
65
|
+
# "post", "keywords" => [...], "match" => "word" }: kinds one or more
|
|
66
|
+
# of "comment", "dm", "story_reply"; keywords up to 20 of 60
|
|
67
|
+
# characters, required unless match is "any"; match is "word",
|
|
68
|
+
# "contains" or "any". `limits` is { "max_sends" => 500, "stop_at" =>
|
|
69
|
+
# "2026-12-31T00:00:00Z" } (both optional). `status` is "active"
|
|
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.
|
|
75
|
+
#
|
|
76
|
+
# Requires the automations:write scope (opt-in on the API key).
|
|
77
|
+
# Errors: 400 validation_error, 400 post_not_instagram, 400
|
|
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).
|
|
84
|
+
def create(actions:, trigger: nil, limits: nil, name: nil, post_id: nil,
|
|
85
|
+
instagram_media_id: nil, status: nil, platform: nil,
|
|
86
|
+
facebook_post_id: nil)
|
|
87
|
+
body = automation_body(
|
|
88
|
+
name: name, post_id: post_id, instagram_media_id: instagram_media_id,
|
|
89
|
+
trigger: trigger, actions: actions, limits: limits, status: status,
|
|
90
|
+
platform: platform, facebook_post_id: facebook_post_id
|
|
91
|
+
)
|
|
92
|
+
@client.request("POST", "/automations", json: body)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# PATCH /automations/{id} - partial update.
|
|
96
|
+
#
|
|
97
|
+
# Each section (trigger, actions, limits) is merged into the stored
|
|
98
|
+
# one; keys you omit keep their value: new trigger "keywords" keep the
|
|
99
|
+
# kinds, the scope and the match; a new actions "dm" "text" keeps the
|
|
100
|
+
# public reply; limits: { "stop_at" => ... } keeps max_sends.
|
|
101
|
+
# "public_reply" => { "enabled" => false } or { "variations" => [] }
|
|
102
|
+
# turns the public reply off; limits: { "max_sends" => nil } removes
|
|
103
|
+
# that limit. status: "paused" pauses and "active" resumes; another
|
|
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
|
|
108
|
+
# "completed" automation starts again when its limits or trigger
|
|
109
|
+
# change. Requires the
|
|
110
|
+
# automations:write scope. 400 no_fields when nothing is passed.
|
|
111
|
+
def update(automation_id, name: nil, post_id: nil, instagram_media_id: nil,
|
|
112
|
+
trigger: nil, actions: nil, limits: nil, status: nil,
|
|
113
|
+
platform: nil, facebook_post_id: nil)
|
|
114
|
+
body = automation_body(
|
|
115
|
+
name: name, post_id: post_id, instagram_media_id: instagram_media_id,
|
|
116
|
+
trigger: trigger, actions: actions, limits: limits, status: status,
|
|
117
|
+
platform: platform, facebook_post_id: facebook_post_id
|
|
118
|
+
)
|
|
119
|
+
@client.request("PATCH", "/automations/#{automation_id}", json: body)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# DELETE /automations/{id} - delete an automation. Returns nil (204).
|
|
123
|
+
# Requires the automations:write scope.
|
|
124
|
+
def delete(automation_id)
|
|
125
|
+
@client.request("DELETE", "/automations/#{automation_id}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# GET /automations/{id}/events - the automation's activity: one event
|
|
129
|
+
# per person it answered, skipped or failed on. outcome filters on
|
|
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.
|
|
134
|
+
#
|
|
135
|
+
# The response is not the plain list envelope: { "data" => [...],
|
|
136
|
+
# "counts" => { "sent" => 12, "skipped" => 3 }, "pagination" => {...} },
|
|
137
|
+
# where counts is the number of events per outcome (an outcome with no
|
|
138
|
+
# events is left out). Each event carries kind ("comment", "dm",
|
|
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),
|
|
144
|
+
# public_reply, platform_message_id, created_at and sent_at.
|
|
145
|
+
def list_events(automation_id, outcome: nil, limit: nil, offset: nil)
|
|
146
|
+
@client.request(
|
|
147
|
+
"GET", "/automations/#{automation_id}/events",
|
|
148
|
+
query: { "outcome" => outcome, "limit" => limit, "offset" => offset }
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def automation_body(name:, post_id:, instagram_media_id:, trigger:,
|
|
155
|
+
actions:, limits:, status:, platform: nil,
|
|
156
|
+
facebook_post_id: nil)
|
|
157
|
+
Internal.drop_nil(
|
|
158
|
+
{
|
|
159
|
+
"platform" => platform,
|
|
160
|
+
"name" => name,
|
|
161
|
+
"post_id" => post_id,
|
|
162
|
+
"instagram_media_id" => instagram_media_id,
|
|
163
|
+
"facebook_post_id" => facebook_post_id,
|
|
164
|
+
"trigger" => trigger,
|
|
165
|
+
"actions" => actions,
|
|
166
|
+
"limits" => limits,
|
|
167
|
+
"status" => status
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -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:
|
|
222
|
-
# to `reply` on comment threads, its "conversation_id" is what
|
|
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
|
|
@@ -8,7 +8,8 @@ module OmniSocials
|
|
|
8
8
|
@client = client
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
# GET /media - list library files.
|
|
11
|
+
# GET /media - list library files. Each item carries "width" and
|
|
12
|
+
# "height" (pixels; nil for videos and older files).
|
|
12
13
|
def list(limit: nil, offset: nil, search: nil, folder_id: nil)
|
|
13
14
|
@client.request(
|
|
14
15
|
"GET", "/media",
|
|
@@ -98,6 +99,16 @@ module OmniSocials
|
|
|
98
99
|
#
|
|
99
100
|
# Provide one of: a public `url`, an existing `media_id`, or
|
|
100
101
|
# `size_bytes` + `mime`.
|
|
102
|
+
#
|
|
103
|
+
# The result carries "file" (size_bytes, mime, size_known, width,
|
|
104
|
+
# height; the pixel size is known for library images, nil otherwise),
|
|
105
|
+
# "connected_platforms", "compatible", "warnings", "summary" and "fit":
|
|
106
|
+
# one entry per connected channel with a fit rule for an IMAGE as a
|
|
107
|
+
# feed post (platform, display_name, fits, ratio, shown = "whole" |
|
|
108
|
+
# "cut", target, default_action = "crop" | "keep" | nil, message).
|
|
109
|
+
# fits false = the ratio is outside the window the channel shows
|
|
110
|
+
# uncropped; default_action is what a post create does unless the
|
|
111
|
+
# post's media_fit says otherwise. Empty for videos and unknown sizes.
|
|
101
112
|
def check(url: nil, media_id: nil, size_bytes: nil, mime: nil)
|
|
102
113
|
body = Internal.drop_nil(
|
|
103
114
|
{
|
|
@@ -46,6 +46,25 @@ module OmniSocials
|
|
|
46
46
|
# POST /posts/create - create a post (draft, or scheduled when
|
|
47
47
|
# scheduled_at is set).
|
|
48
48
|
#
|
|
49
|
+
# draft: true with scheduled_at stores a planned draft: the post is
|
|
50
|
+
# "draft" and keeps that time, shows on the dashboard calendar at that
|
|
51
|
+
# slot with a dashed outline, and the scheduler never publishes it. A
|
|
52
|
+
# person schedules it from the composer, or you do with #update and
|
|
53
|
+
# draft: false. Use it when an agent proposes a slot that a human
|
|
54
|
+
# should confirm. Not allowed with publish_now or approval_workflow_id
|
|
55
|
+
# (400 validation_error). Without scheduled_at it changes nothing.
|
|
56
|
+
#
|
|
57
|
+
# media_fit is the image fit per channel for feed images whose ratio is
|
|
58
|
+
# outside what a channel shows uncropped: a Hash of platform id
|
|
59
|
+
# ("instagram", "pinterest", ...) to "auto", "crop" or "keep". auto
|
|
60
|
+
# (default): Instagram crops a tall or wide image to 4:5 / 1.91:1 around
|
|
61
|
+
# the subject (Instagram cuts it to that window anyway and Meta cannot
|
|
62
|
+
# boost a feed post outside it); Pinterest keeps a Pin taller than 2:3
|
|
63
|
+
# and reports it. crop / keep force it per channel. Every decision comes
|
|
64
|
+
# back in the same "warnings" array with code "image_fit" (action is
|
|
65
|
+
# "cropped", "kept" or "crop_failed", with platform, media_url, ratio,
|
|
66
|
+
# target and crop_url).
|
|
67
|
+
#
|
|
49
68
|
# approval_workflow_id (a workflow id from client.approval_workflows.list)
|
|
50
69
|
# routes the post through a saved approval workflow: it is created as
|
|
51
70
|
# in_approval (approval_status "pending") instead of scheduled, the
|
|
@@ -87,6 +106,11 @@ module OmniSocials
|
|
|
87
106
|
# 400 validation_error asking you to reconnect Threads when the
|
|
88
107
|
# connection lacks the threads_location_tagging permission).
|
|
89
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
|
+
#
|
|
90
114
|
# Pinterest posts can tag products: pass
|
|
91
115
|
# pinterest: { "board_id" => "...", "product_tags" => [...] } with up
|
|
92
116
|
# to 24 product Pins of the connected Pinterest account, each as a Pin
|
|
@@ -96,6 +120,55 @@ module OmniSocials
|
|
|
96
120
|
# and the Post's "pinterest" block then carries "product_tags_result"
|
|
97
121
|
# (requested, tagged, skipped, error). More than 24 entries or an
|
|
98
122
|
# invalid entry raises a 400 validation_error.
|
|
123
|
+
#
|
|
124
|
+
# telegram holds the Telegram options (the connected channel or group,
|
|
125
|
+
# posted by the OmniSocials bot): "silent", "link_preview", "pin",
|
|
126
|
+
# "protect_content", "spoiler", "caption_above_media" (booleans), up to
|
|
127
|
+
# 3 "buttons" ({ "text" => ..., "url" => ... }, not on an album or a
|
|
128
|
+
# poll), a native "poll" ({ "options" => 2 to 12 of up to 100
|
|
129
|
+
# characters, "open_period" => 5 to 2628000 seconds, "multiple" => true
|
|
130
|
+
# }; the post text is the question, max 300 characters, no media) and
|
|
131
|
+
# "first_comment" (a reply under the post). Text up to 4,096
|
|
132
|
+
# characters; with media, text over 1,024 characters is sent as a reply
|
|
133
|
+
# under the media. No Stories. On #update every key merges into the
|
|
134
|
+
# stored options. Once sent, the Post's "telegram" block carries
|
|
135
|
+
# "result" (message ids and links).
|
|
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
|
+
#
|
|
154
|
+
# comment_dm_automation binds a comment-to-DM automation to the post
|
|
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
|
|
167
|
+
# "comment_dm_automation: ". The response then carries
|
|
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.
|
|
99
172
|
def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
|
|
100
173
|
media_urls: nil, type: nil, source: nil, link_url: nil,
|
|
101
174
|
link_title: nil, link_description: nil, link_thumbnail_url: nil,
|
|
@@ -105,7 +178,9 @@ module OmniSocials
|
|
|
105
178
|
instagram: nil, facebook: nil, linkedin: nil,
|
|
106
179
|
linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
|
|
107
180
|
mastodon: nil, threads: nil, google_business: nil,
|
|
108
|
-
|
|
181
|
+
telegram: nil, discord: nil, linkedin_poll: nil,
|
|
182
|
+
approval_workflow_id: nil, draft: nil, media_fit: nil,
|
|
183
|
+
comment_dm_automation: nil)
|
|
109
184
|
body = create_body(
|
|
110
185
|
content: content, channels: channels, scheduled_at: scheduled_at,
|
|
111
186
|
media_ids: media_ids, media_urls: media_urls, type: type,
|
|
@@ -118,15 +193,19 @@ module OmniSocials
|
|
|
118
193
|
youtube: youtube, instagram: instagram, facebook: facebook,
|
|
119
194
|
linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
|
|
120
195
|
x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
|
|
121
|
-
google_business: google_business,
|
|
122
|
-
|
|
196
|
+
google_business: google_business, telegram: telegram,
|
|
197
|
+
discord: discord, linkedin_poll: linkedin_poll,
|
|
198
|
+
approval_workflow_id: approval_workflow_id, draft: draft,
|
|
199
|
+
media_fit: media_fit, comment_dm_automation: comment_dm_automation
|
|
123
200
|
)
|
|
124
201
|
@client.request("POST", "/posts/create", json: body)
|
|
125
202
|
end
|
|
126
203
|
|
|
127
204
|
# POST /posts/create-and-publish - create and publish immediately.
|
|
128
|
-
# See #create for the "warnings" array and the
|
|
129
|
-
# "
|
|
205
|
+
# See #create for the "warnings" array (X link posts, and the
|
|
206
|
+
# "image_fit" entries steered by media_fit), the 402
|
|
207
|
+
# "x_credits_insufficient" credit gate on X link posts and
|
|
208
|
+
# comment_dm_automation.
|
|
130
209
|
def create_and_publish(content:, channels: nil, media_ids: nil,
|
|
131
210
|
media_urls: nil, type: nil, source: nil,
|
|
132
211
|
link_url: nil, link_title: nil, link_description: nil,
|
|
@@ -138,7 +217,9 @@ module OmniSocials
|
|
|
138
217
|
facebook: nil, linkedin: nil, linkedin_page: nil,
|
|
139
218
|
tiktok: nil, x: nil, bluesky: nil, mastodon: nil,
|
|
140
219
|
threads: nil, google_business: nil,
|
|
141
|
-
linkedin_poll: nil
|
|
220
|
+
telegram: nil, discord: nil, linkedin_poll: nil,
|
|
221
|
+
media_fit: nil,
|
|
222
|
+
comment_dm_automation: nil)
|
|
142
223
|
body = create_body(
|
|
143
224
|
content: content, channels: channels, scheduled_at: nil,
|
|
144
225
|
media_ids: media_ids, media_urls: media_urls, type: type,
|
|
@@ -151,13 +232,29 @@ module OmniSocials
|
|
|
151
232
|
youtube: youtube, instagram: instagram, facebook: facebook,
|
|
152
233
|
linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
|
|
153
234
|
x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
|
|
154
|
-
google_business: google_business,
|
|
235
|
+
google_business: google_business, telegram: telegram,
|
|
236
|
+
discord: discord, linkedin_poll: linkedin_poll,
|
|
237
|
+
media_fit: media_fit, comment_dm_automation: comment_dm_automation
|
|
155
238
|
)
|
|
156
239
|
@client.request("POST", "/posts/create-and-publish", json: body)
|
|
157
240
|
end
|
|
158
241
|
|
|
159
242
|
# PATCH /posts/{id} - update a draft or scheduled post.
|
|
160
243
|
#
|
|
244
|
+
# draft: false without scheduled_at schedules a planned draft at the
|
|
245
|
+
# scheduled_at it already carries (400 validation_error when it has
|
|
246
|
+
# none or it is in the past). draft: true without scheduled_at
|
|
247
|
+
# unschedules a scheduled post: it keeps its time and stays on the
|
|
248
|
+
# calendar as a dashed draft, nothing publishes; a post waiting for
|
|
249
|
+
# approval leaves its workflow. draft: true with scheduled_at moves the
|
|
250
|
+
# time and keeps (or makes) the post a draft.
|
|
251
|
+
#
|
|
252
|
+
# media_fit (same shape as on #create) alone re-decides the stored
|
|
253
|
+
# images: "keep" gives the original back, "crop" crops an image that
|
|
254
|
+
# was kept. Like #create, the response can carry a top-level "warnings"
|
|
255
|
+
# array with "image_fit" entries when the update changes the images or
|
|
256
|
+
# media_fit.
|
|
257
|
+
#
|
|
161
258
|
# Only top-level nils are dropped from the body, so passing e.g.
|
|
162
259
|
# x: { "thread_parts" => nil } still clears an X thread (reverts the
|
|
163
260
|
# post to single-tweet mode). The same applies to bluesky, mastodon
|
|
@@ -169,13 +266,24 @@ module OmniSocials
|
|
|
169
266
|
#
|
|
170
267
|
# See #create for the 402 "x_credits_insufficient" credit gate that
|
|
171
268
|
# can also refuse an update to a scheduled X link post.
|
|
269
|
+
#
|
|
270
|
+
# comment_dm_automation (same shape as on #create) as a Hash replaces
|
|
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.
|
|
172
278
|
def update(post_id, content: nil, scheduled_at: nil, channels: nil,
|
|
173
279
|
media_ids: nil, media_urls: nil, type: nil, location_id: nil,
|
|
174
280
|
collaborators: nil, user_tags: nil, pinterest: nil,
|
|
175
281
|
youtube: nil, instagram: nil, facebook: nil, linkedin: nil,
|
|
176
282
|
linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
|
|
177
283
|
mastodon: nil, threads: nil, google_business: nil,
|
|
178
|
-
|
|
284
|
+
telegram: nil, discord: OmniSocials::NOT_GIVEN,
|
|
285
|
+
linkedin_poll: nil, draft: nil, media_fit: nil,
|
|
286
|
+
comment_dm_automation: OmniSocials::NOT_GIVEN)
|
|
179
287
|
body = Internal.drop_nil(
|
|
180
288
|
{
|
|
181
289
|
"content" => content,
|
|
@@ -199,9 +307,17 @@ module OmniSocials
|
|
|
199
307
|
"mastodon" => mastodon,
|
|
200
308
|
"threads" => threads,
|
|
201
309
|
"google_business" => google_business,
|
|
202
|
-
"
|
|
310
|
+
"telegram" => telegram,
|
|
311
|
+
"linkedin_poll" => linkedin_poll,
|
|
312
|
+
"draft" => draft,
|
|
313
|
+
"media_fit" => media_fit
|
|
203
314
|
}
|
|
204
315
|
)
|
|
316
|
+
# An explicit nil removes the post's automation, so it must survive
|
|
317
|
+
# drop_nil; NOT_GIVEN (the default) leaves it untouched.
|
|
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)
|
|
205
321
|
@client.request("PATCH", "/posts/#{post_id}", json: body)
|
|
206
322
|
end
|
|
207
323
|
|
|
@@ -277,7 +393,9 @@ module OmniSocials
|
|
|
277
393
|
pinterest:, youtube:, instagram:, facebook:, linkedin:,
|
|
278
394
|
linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
|
|
279
395
|
threads:, google_business:, linkedin_poll:,
|
|
280
|
-
approval_workflow_id: nil
|
|
396
|
+
telegram: nil, discord: nil, approval_workflow_id: nil,
|
|
397
|
+
draft: nil,
|
|
398
|
+
media_fit: nil, comment_dm_automation: nil)
|
|
281
399
|
Internal.drop_nil(
|
|
282
400
|
{
|
|
283
401
|
"content" => content,
|
|
@@ -310,8 +428,13 @@ module OmniSocials
|
|
|
310
428
|
"mastodon" => mastodon,
|
|
311
429
|
"threads" => threads,
|
|
312
430
|
"google_business" => google_business,
|
|
431
|
+
"telegram" => telegram,
|
|
432
|
+
"discord" => discord,
|
|
313
433
|
"linkedin_poll" => linkedin_poll,
|
|
314
|
-
"approval_workflow_id" => approval_workflow_id
|
|
434
|
+
"approval_workflow_id" => approval_workflow_id,
|
|
435
|
+
"draft" => draft,
|
|
436
|
+
"media_fit" => media_fit,
|
|
437
|
+
"comment_dm_automation" => comment_dm_automation
|
|
315
438
|
}
|
|
316
439
|
)
|
|
317
440
|
end
|
data/lib/omnisocials/version.rb
CHANGED
data/lib/omnisocials.rb
CHANGED
|
@@ -26,6 +26,7 @@ require_relative "omnisocials/resources/analytics"
|
|
|
26
26
|
require_relative "omnisocials/resources/audio"
|
|
27
27
|
require_relative "omnisocials/resources/locations"
|
|
28
28
|
require_relative "omnisocials/resources/pinterest"
|
|
29
|
+
require_relative "omnisocials/resources/automations"
|
|
29
30
|
require_relative "omnisocials/resources/inbox"
|
|
30
31
|
require_relative "omnisocials/resources/webhooks"
|
|
31
32
|
require_relative "omnisocials/client"
|
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
|
+
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-
|
|
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,
|
|
@@ -29,6 +29,7 @@ files:
|
|
|
29
29
|
- lib/omnisocials/resources/analytics.rb
|
|
30
30
|
- lib/omnisocials/resources/approval_workflows.rb
|
|
31
31
|
- lib/omnisocials/resources/audio.rb
|
|
32
|
+
- lib/omnisocials/resources/automations.rb
|
|
32
33
|
- lib/omnisocials/resources/folders.rb
|
|
33
34
|
- lib/omnisocials/resources/hashtag_sets.rb
|
|
34
35
|
- lib/omnisocials/resources/inbox.rb
|
|
@@ -39,11 +40,11 @@ files:
|
|
|
39
40
|
- lib/omnisocials/resources/webhooks.rb
|
|
40
41
|
- lib/omnisocials/version.rb
|
|
41
42
|
- lib/omnisocials/webhooks.rb
|
|
42
|
-
homepage: https://omnisocials.com
|
|
43
|
+
homepage: https://omnisocials.com/sdk/ruby
|
|
43
44
|
licenses:
|
|
44
45
|
- MIT
|
|
45
46
|
metadata:
|
|
46
|
-
homepage_uri: https://omnisocials.com
|
|
47
|
+
homepage_uri: https://omnisocials.com/sdk/ruby
|
|
47
48
|
documentation_uri: https://docs.omnisocials.com
|
|
48
49
|
source_code_uri: https://github.com/OmniSocials/omnisocials-ruby
|
|
49
50
|
bug_tracker_uri: https://github.com/OmniSocials/omnisocials-ruby/issues
|