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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26f8bcb600108496ff4cf072d383bf2dbb1082c573b14d2f0e7b8986284c1b1d
4
- data.tar.gz: e42dc4799da58ada826fc37d34cab90c96d91b92afed5117f89dce73f01cdf2c
3
+ metadata.gz: 712ed8026c6e6223f5d2bbcaaa0696cda2ed181ab8f4da6bd2b95fd2430bc84e
4
+ data.tar.gz: 2e1cf587a451e8f966800ddef4f74b1b8a0422e0df7fd7ffb078afdf5e1636eb
5
5
  SHA512:
6
- metadata.gz: 92398e94cbc636eb8dfede06d8cb2dfc0d5a7239c39ef72e417b6f8de5e982ed8538c4f5d7f61c6bdb70cfc2e52a76a556d58d27bd6c5cf161c784d6a30b824f
7
- data.tar.gz: 1a5e379ad7bafe61ba70912d724531670ac7245e44c5d45e4e4968bfb458f0b030ab9a03ec64001fa3ddd6b50021c4854df280af220b1c6413ce17b30ae30d3d
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 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 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 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,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["data"]["best_times"].each { |slot| puts slot }
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 - top up and re-enable it in the dashboard."
594
+ puts "X inbox is suspended until a top-up."
483
595
  else
484
596
  raise
485
597
  end
@@ -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,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: 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
@@ -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
- linkedin_poll: nil, approval_workflow_id: nil)
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, linkedin_poll: linkedin_poll,
122
- approval_workflow_id: approval_workflow_id
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 402
129
- # "x_credits_insufficient" credit gate on X link posts.
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, linkedin_poll: linkedin_poll
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
- linkedin_poll: nil)
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
- "linkedin_poll" => linkedin_poll
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.9.0"
4
+ VERSION = "0.13.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.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-05 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,
@@ -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