omnisocials 0.9.0 → 0.11.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: 26f8bcb600108496ff4cf072d383bf2dbb1082c573b14d2f0e7b8986284c1b1d
4
- data.tar.gz: e42dc4799da58ada826fc37d34cab90c96d91b92afed5117f89dce73f01cdf2c
3
+ metadata.gz: 473f213709ed52c13f70cd2b84c26b7d92c2355731dfc0fb676d3b0f3949fbac
4
+ data.tar.gz: 423f50628c3c4dae0b98d3c486337a573cb32972af6cdf74cd41a8646899f162
5
5
  SHA512:
6
- metadata.gz: 92398e94cbc636eb8dfede06d8cb2dfc0d5a7239c39ef72e417b6f8de5e982ed8538c4f5d7f61c6bdb70cfc2e52a76a556d58d27bd6c5cf161c784d6a30b824f
7
- data.tar.gz: 1a5e379ad7bafe61ba70912d724531670ac7245e44c5d45e4e4968bfb458f0b030ab9a03ec64001fa3ddd6b50021c4854df280af220b1c6413ce17b30ae30d3d
6
+ metadata.gz: 5a047afd2262cd53375d22fef8b7c79ed254fddf8e5057cdb4cdd909308821bc19c81b73cddf04ad736cecca998bf0ee0f974f6205d2e0bd4472547ee4464dfa
7
+ data.tar.gz: 27c340cc9e7954eae71506ed026b10b1421130cb59c283cab8fdbac3016759b5e449c406367579acbc3cf4c4c1b70fb151750350d8e6bbb27fe330eaaf49cba3
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OmniSocials Ruby SDK
2
2
 
3
- The official Ruby client for the [OmniSocials](https://omnisocials.com) Public API. Schedule and publish posts, upload media, and read analytics across Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads, Mastodon, and Google Business.
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)
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 look like `omsk_live_...` (or `omsk_test_...` for test keys) and are bound to one workspace.
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,54 @@ 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`). 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
+
68
116
  ### Per-media alt text
69
117
 
70
118
  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 +151,32 @@ post = client.posts.create(
103
151
 
104
152
  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
153
 
154
+ ### Telegram channels and groups
155
+
156
+ `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.
157
+
158
+ ```ruby
159
+ client.posts.create(
160
+ content: "The new collection is live. Tap the button to shop.",
161
+ channels: ["telegram"],
162
+ scheduled_at: "2026-11-02T09:00:00Z",
163
+ media_urls: ["https://example.com/collection.jpg"],
164
+ telegram: {
165
+ "silent" => true,
166
+ "pin" => true,
167
+ "buttons" => [{ "text" => "Shop now", "url" => "https://example.com/shop" }],
168
+ "first_comment" => "Questions? Reply to this message."
169
+ }
170
+ )
171
+
172
+ # A native poll: the text is the question.
173
+ client.posts.create(
174
+ content: "Which day suits the live Q&A?",
175
+ channels: ["telegram"],
176
+ telegram: { "poll" => { "options" => %w[Tuesday Thursday Saturday], "open_period" => 86_400 } }
177
+ )
178
+ ```
179
+
106
180
  ### Chained threads (X, Bluesky, Mastodon, Threads)
107
181
 
108
182
  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):
@@ -350,9 +424,11 @@ accounts = client.analytics.accounts(platform: "instagram")
350
424
 
351
425
  ### Best times to post
352
426
 
427
+ The response is not wrapped in `data`. `platform`, `timezone`, `basis`, `grid` and `recommendations` sit at the top level.
428
+
353
429
  ```ruby
354
430
  best = client.analytics.best_times(platform: "instagram", timezone: "Europe/Amsterdam")
355
- best["data"]["best_times"].each { |slot| puts slot }
431
+ best["recommendations"].each { |slot| puts "#{slot["day"]} #{slot["time"]} #{slot["score"]}" }
356
432
  ```
357
433
 
358
434
  ## Locations (Instagram and Threads place tagging)
@@ -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, :inbox,
64
- :webhooks
63
+ :accounts, :analytics, :audio, :locations, :pinterest,
64
+ :automations, :inbox, :webhooks
65
65
 
66
- # api_key - API key (omsk_live_* / omsk_test_*). Falls back to the
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,135 @@
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. 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
11
+ # comment_dm_automation on posts).
12
+ class Automations
13
+ def initialize(client)
14
+ @client = client
15
+ end
16
+
17
+ # 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)
22
+ @client.request(
23
+ "GET", "/automations",
24
+ query: { "status" => status, "post_id" => post_id }
25
+ )
26
+ end
27
+
28
+ # GET /automations/{id} - fetch a single automation. Requires the
29
+ # automations:read scope (opt-in on the API key).
30
+ def get(automation_id)
31
+ @client.request("GET", "/automations/#{automation_id}")
32
+ end
33
+
34
+ # POST /automations - create a comment-to-DM automation.
35
+ #
36
+ # A keyword in an Instagram comment (or a DM, or a story reply) is
37
+ # 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.
42
+ #
43
+ # `actions` is required: { "dm" => { "text" => "..." } } (max 1000
44
+ # characters) plus an optional "public_reply" => { "enabled" => true,
45
+ # "variations" => [...] } (comment kind only, up to 10 of 300
46
+ # characters). `trigger` is { "kinds" => ["comment"], "scope" =>
47
+ # "post", "keywords" => [...], "match" => "word" }: kinds one or more
48
+ # of "comment", "dm", "story_reply"; keywords up to 20 of 60
49
+ # characters, required unless match is "any"; match is "word",
50
+ # "contains" or "any". `limits` is { "max_sends" => 500, "stop_at" =>
51
+ # "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.
55
+ #
56
+ # Requires the automations:write scope (opt-in on the API key).
57
+ # 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).
61
+ def create(actions:, trigger: nil, limits: nil, name: nil, post_id: nil,
62
+ instagram_media_id: nil, status: nil)
63
+ body = automation_body(
64
+ name: name, post_id: post_id, instagram_media_id: instagram_media_id,
65
+ trigger: trigger, actions: actions, limits: limits, status: status
66
+ )
67
+ @client.request("POST", "/automations", json: body)
68
+ end
69
+
70
+ # PATCH /automations/{id} - partial update.
71
+ #
72
+ # Each section (trigger, actions, limits) is merged into the stored
73
+ # one; keys you omit keep their value: new trigger "keywords" keep the
74
+ # kinds, the scope and the match; a new actions "dm" "text" keeps the
75
+ # public reply; limits: { "stop_at" => ... } keeps max_sends.
76
+ # "public_reply" => { "enabled" => false } or { "variations" => [] }
77
+ # turns the public reply off; limits: { "max_sends" => nil } removes
78
+ # that limit. status: "paused" pauses and "active" resumes; another
79
+ # post_id / instagram_media_id moves the automation to that post. A
80
+ # "completed" automation starts again when its limits or trigger
81
+ # change. Requires the automations:write scope. 400 no_fields when
82
+ # nothing is passed.
83
+ def update(automation_id, name: nil, post_id: nil, instagram_media_id: nil,
84
+ trigger: nil, actions: nil, limits: nil, status: nil)
85
+ body = automation_body(
86
+ name: name, post_id: post_id, instagram_media_id: instagram_media_id,
87
+ trigger: trigger, actions: actions, limits: limits, status: status
88
+ )
89
+ @client.request("PATCH", "/automations/#{automation_id}", json: body)
90
+ end
91
+
92
+ # DELETE /automations/{id} - delete an automation. Returns nil (204).
93
+ # Requires the automations:write scope.
94
+ def delete(automation_id)
95
+ @client.request("DELETE", "/automations/#{automation_id}")
96
+ end
97
+
98
+ # GET /automations/{id}/events - the automation's activity: one event
99
+ # 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.
102
+ #
103
+ # The response is not the plain list envelope: { "data" => [...],
104
+ # "counts" => { "sent" => 12, "skipped" => 3 }, "pagination" => {...} },
105
+ # where counts is the number of events per outcome (an outcome with no
106
+ # 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),
109
+ # public_reply, platform_message_id, created_at and sent_at.
110
+ def list_events(automation_id, outcome: nil, limit: nil, offset: nil)
111
+ @client.request(
112
+ "GET", "/automations/#{automation_id}/events",
113
+ query: { "outcome" => outcome, "limit" => limit, "offset" => offset }
114
+ )
115
+ end
116
+
117
+ private
118
+
119
+ def automation_body(name:, post_id:, instagram_media_id:, trigger:,
120
+ actions:, limits:, status:)
121
+ Internal.drop_nil(
122
+ {
123
+ "name" => name,
124
+ "post_id" => post_id,
125
+ "instagram_media_id" => instagram_media_id,
126
+ "trigger" => trigger,
127
+ "actions" => actions,
128
+ "limits" => limits,
129
+ "status" => status
130
+ }
131
+ )
132
+ end
133
+ end
134
+ end
135
+ end
@@ -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
@@ -96,6 +115,33 @@ module OmniSocials
96
115
  # and the Post's "pinterest" block then carries "product_tags_result"
97
116
  # (requested, tagged, skipped, error). More than 24 entries or an
98
117
  # invalid entry raises a 400 validation_error.
118
+ #
119
+ # telegram holds the Telegram options (the connected channel or group,
120
+ # posted by the OmniSocials bot): "silent", "link_preview", "pin",
121
+ # "protect_content", "spoiler", "caption_above_media" (booleans), up to
122
+ # 3 "buttons" ({ "text" => ..., "url" => ... }, not on an album or a
123
+ # poll), a native "poll" ({ "options" => 2 to 12 of up to 100
124
+ # characters, "open_period" => 5 to 2628000 seconds, "multiple" => true
125
+ # }; the post text is the question, max 300 characters, no media) and
126
+ # "first_comment" (a reply under the post). Text up to 4,096
127
+ # characters; with media, text over 1,024 characters is sent as a reply
128
+ # under the media. No Stories. On #update every key merges into the
129
+ # stored options. Once sent, the Post's "telegram" block carries
130
+ # "result" (message ids and links).
131
+ #
132
+ # 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
142
+ # "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.
99
145
  def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
100
146
  media_urls: nil, type: nil, source: nil, link_url: nil,
101
147
  link_title: nil, link_description: nil, link_thumbnail_url: nil,
@@ -105,7 +151,9 @@ module OmniSocials
105
151
  instagram: nil, facebook: nil, linkedin: nil,
106
152
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
107
153
  mastodon: nil, threads: nil, google_business: nil,
108
- linkedin_poll: nil, approval_workflow_id: nil)
154
+ telegram: nil, linkedin_poll: nil,
155
+ approval_workflow_id: nil, draft: nil, media_fit: nil,
156
+ comment_dm_automation: nil)
109
157
  body = create_body(
110
158
  content: content, channels: channels, scheduled_at: scheduled_at,
111
159
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -118,15 +166,19 @@ module OmniSocials
118
166
  youtube: youtube, instagram: instagram, facebook: facebook,
119
167
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
120
168
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
121
- google_business: google_business, linkedin_poll: linkedin_poll,
122
- approval_workflow_id: approval_workflow_id
169
+ google_business: google_business, telegram: telegram,
170
+ linkedin_poll: linkedin_poll,
171
+ approval_workflow_id: approval_workflow_id, draft: draft,
172
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation
123
173
  )
124
174
  @client.request("POST", "/posts/create", json: body)
125
175
  end
126
176
 
127
177
  # POST /posts/create-and-publish - create and publish immediately.
128
- # See #create for the "warnings" array and the 402
129
- # "x_credits_insufficient" credit gate on X link posts.
178
+ # See #create for the "warnings" array (X link posts, and the
179
+ # "image_fit" entries steered by media_fit), the 402
180
+ # "x_credits_insufficient" credit gate on X link posts and
181
+ # comment_dm_automation.
130
182
  def create_and_publish(content:, channels: nil, media_ids: nil,
131
183
  media_urls: nil, type: nil, source: nil,
132
184
  link_url: nil, link_title: nil, link_description: nil,
@@ -138,7 +190,8 @@ module OmniSocials
138
190
  facebook: nil, linkedin: nil, linkedin_page: nil,
139
191
  tiktok: nil, x: nil, bluesky: nil, mastodon: nil,
140
192
  threads: nil, google_business: nil,
141
- linkedin_poll: nil)
193
+ telegram: nil, linkedin_poll: nil, media_fit: nil,
194
+ comment_dm_automation: nil)
142
195
  body = create_body(
143
196
  content: content, channels: channels, scheduled_at: nil,
144
197
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -151,13 +204,29 @@ module OmniSocials
151
204
  youtube: youtube, instagram: instagram, facebook: facebook,
152
205
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
153
206
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
154
- google_business: google_business, linkedin_poll: linkedin_poll
207
+ google_business: google_business, telegram: telegram,
208
+ linkedin_poll: linkedin_poll,
209
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation
155
210
  )
156
211
  @client.request("POST", "/posts/create-and-publish", json: body)
157
212
  end
158
213
 
159
214
  # PATCH /posts/{id} - update a draft or scheduled post.
160
215
  #
216
+ # draft: false without scheduled_at schedules a planned draft at the
217
+ # scheduled_at it already carries (400 validation_error when it has
218
+ # none or it is in the past). draft: true without scheduled_at
219
+ # unschedules a scheduled post: it keeps its time and stays on the
220
+ # calendar as a dashed draft, nothing publishes; a post waiting for
221
+ # approval leaves its workflow. draft: true with scheduled_at moves the
222
+ # time and keeps (or makes) the post a draft.
223
+ #
224
+ # media_fit (same shape as on #create) alone re-decides the stored
225
+ # images: "keep" gives the original back, "crop" crops an image that
226
+ # was kept. Like #create, the response can carry a top-level "warnings"
227
+ # array with "image_fit" entries when the update changes the images or
228
+ # media_fit.
229
+ #
161
230
  # Only top-level nils are dropped from the body, so passing e.g.
162
231
  # x: { "thread_parts" => nil } still clears an X thread (reverts the
163
232
  # post to single-tweet mode). The same applies to bluesky, mastodon
@@ -169,13 +238,19 @@ module OmniSocials
169
238
  #
170
239
  # See #create for the 402 "x_credits_insufficient" credit gate that
171
240
  # can also refuse an update to a scheduled X link post.
241
+ #
242
+ # 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.
172
245
  def update(post_id, content: nil, scheduled_at: nil, channels: nil,
173
246
  media_ids: nil, media_urls: nil, type: nil, location_id: nil,
174
247
  collaborators: nil, user_tags: nil, pinterest: nil,
175
248
  youtube: nil, instagram: nil, facebook: nil, linkedin: nil,
176
249
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
177
250
  mastodon: nil, threads: nil, google_business: nil,
178
- linkedin_poll: nil)
251
+ telegram: nil, linkedin_poll: nil, draft: nil,
252
+ media_fit: nil,
253
+ comment_dm_automation: OmniSocials::NOT_GIVEN)
179
254
  body = Internal.drop_nil(
180
255
  {
181
256
  "content" => content,
@@ -199,9 +274,15 @@ module OmniSocials
199
274
  "mastodon" => mastodon,
200
275
  "threads" => threads,
201
276
  "google_business" => google_business,
202
- "linkedin_poll" => linkedin_poll
277
+ "telegram" => telegram,
278
+ "linkedin_poll" => linkedin_poll,
279
+ "draft" => draft,
280
+ "media_fit" => media_fit
203
281
  }
204
282
  )
283
+ # An explicit nil removes the post's automation, so it must survive
284
+ # drop_nil; NOT_GIVEN (the default) leaves it untouched.
285
+ body["comment_dm_automation"] = comment_dm_automation unless comment_dm_automation.is_a?(NotGiven)
205
286
  @client.request("PATCH", "/posts/#{post_id}", json: body)
206
287
  end
207
288
 
@@ -277,7 +358,8 @@ module OmniSocials
277
358
  pinterest:, youtube:, instagram:, facebook:, linkedin:,
278
359
  linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
279
360
  threads:, google_business:, linkedin_poll:,
280
- approval_workflow_id: nil)
361
+ telegram: nil, approval_workflow_id: nil, draft: nil,
362
+ media_fit: nil, comment_dm_automation: nil)
281
363
  Internal.drop_nil(
282
364
  {
283
365
  "content" => content,
@@ -310,8 +392,12 @@ module OmniSocials
310
392
  "mastodon" => mastodon,
311
393
  "threads" => threads,
312
394
  "google_business" => google_business,
395
+ "telegram" => telegram,
313
396
  "linkedin_poll" => linkedin_poll,
314
- "approval_workflow_id" => approval_workflow_id
397
+ "approval_workflow_id" => approval_workflow_id,
398
+ "draft" => draft,
399
+ "media_fit" => media_fit,
400
+ "comment_dm_automation" => comment_dm_automation
315
401
  }
316
402
  )
317
403
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.9.0"
4
+ VERSION = "0.11.0"
5
5
  end
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.9.0
4
+ version: 0.11.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-05 00:00:00.000000000 Z
11
+ date: 2026-10-21 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