omnisocials 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 473f213709ed52c13f70cd2b84c26b7d92c2355731dfc0fb676d3b0f3949fbac
4
- data.tar.gz: 423f50628c3c4dae0b98d3c486337a573cb32972af6cdf74cd41a8646899f162
3
+ metadata.gz: 27468385685af2b1f49e6665393bb687f2dd9fc18cf7d06b98190c78c9b3c3fe
4
+ data.tar.gz: 24dafc551dc62fd9922aa9ce330ae138288a3ecd86dc768abcf35e2f5f466cae
5
5
  SHA512:
6
- metadata.gz: 5a047afd2262cd53375d22fef8b7c79ed254fddf8e5057cdb4cdd909308821bc19c81b73cddf04ad736cecca998bf0ee0f974f6205d2e0bd4472547ee4464dfa
7
- data.tar.gz: 27c340cc9e7954eae71506ed026b10b1421130cb59c283cab8fdbac3016759b5e449c406367579acbc3cf4c4c1b70fb151750350d8e6bbb27fe330eaaf49cba3
6
+ metadata.gz: 43d61512382a55a5ef87c2e906f28f55153a15d3deed758262e6f919eeb934e5de86be8e700570ffc2bb9862a33e111e285085248ed0bbd7f3645046a3dcf6ce
7
+ data.tar.gz: 93c23762f52f0550bf38fcae55d850aafa24365966adad31d71b603c8c9fbee3c297be8cb31219d947fc2dd38d1d7b7b2252642460ae98d65a024060cb3d16ba
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OmniSocials Ruby SDK
2
2
 
3
- The official Ruby SDK for OmniSocials: post, schedule, reply and report on 12 social platforms (Instagram, Facebook, LinkedIn, X, TikTok, YouTube, Threads, Pinterest, Bluesky, Mastodon, Google Business and Telegram) from one API. Guide: [omnisocials.com/sdk/ruby](https://omnisocials.com/sdk/ruby)
3
+ The official Ruby SDK for OmniSocials: post, schedule, reply and report on 13 social platforms (Instagram, Facebook, LinkedIn, X, TikTok, YouTube, Threads, Pinterest, Bluesky, Mastodon, Google Business, Telegram and Discord) from one API. Guide: [omnisocials.com/sdk/ruby](https://omnisocials.com/sdk/ruby)
4
4
 
5
5
  Full API reference: [https://docs.omnisocials.com](https://docs.omnisocials.com)
6
6
 
@@ -111,7 +111,11 @@ events["data"].each do |event|
111
111
  end
112
112
  ```
113
113
 
114
- The events response is `{ "data", "counts", "pagination" }`, not the plain list: `counts` is the number of events per outcome (`sent`, `skipped`, `failed`, `pending`). An automation can also listen on a post published outside OmniSocials (`client.automations.create(instagram_media_id: ..., actions: ...)`), on the next post the workspace publishes (`trigger: { "scope" => "next_post" }`) or on every post (`"scope" => "any"`), and on DMs and story replies (`trigger: { "kinds" => ["dm", "story_reply"], ... }`).
114
+ The events response is `{ "data", "counts", "pagination" }`, not the plain list: `counts` is the number of events per outcome (`sent`, `skipped`, `failed`, `pending`, `queued`, `opted_out`). An automation can also listen on a post published outside OmniSocials (`client.automations.create(instagram_media_id: ..., actions: ...)`), on the next post the workspace publishes (`trigger: { "scope" => "next_post" }`) or on every post (`"scope" => "any"`), and on DMs and story replies (`trigger: { "kinds" => ["dm", "story_reply"], ... }`).
115
+
116
+ A Facebook Page works the same way, and the DM lands in Messenger: pass `platform: "facebook"` to `client.automations.create`, or `facebook_post_id: ...` for a Page post published outside OmniSocials. On a post, `comment_dm_automation: { "platforms" => ["instagram", "facebook"], ... }` makes one automation per channel with the same keywords and DM. Facebook has no story replies.
117
+
118
+ On X, pass `platform: "x"` with `trigger: { "kinds" => ["dm"], "keywords" => ["guide"] }`: the X account answers a keyword in a DM, once per person, and every answer ends with "Reply STOP to stop automatic messages." An active X automation needs X DMs on for the workspace (`409 x_dms_off`; `status: "paused"` always saves). An answer over the send cap waits in a queue (`queued`) for up to an hour.
115
119
 
116
120
  ### Per-media alt text
117
121
 
@@ -177,6 +181,30 @@ client.posts.create(
177
181
  )
178
182
  ```
179
183
 
184
+ ### Discord channels
185
+
186
+ `discord` is one Discord channel the workspace connected in OmniSocials; the post goes out through the channel's incoming webhook and shows the webhook's name and picture. The `discord` options: `silent`, `link_preview` (default on), `allow_mentions` (default off, so @everyone, @here, role and user mentions show as text and ping nobody) and `spoiler` (booleans), `thread_name` (the post title in a forum or media channel, 1 to 100 characters), up to 5 `buttons` (`{ text, url }`, one row, not with a poll) and a native `poll` (2 to 10 options of up to 55 characters, `duration_hours` 1 to 768, default 24, `multiple`; the post text is the question, max 300 characters, no media). Text up to 2,000 characters. Up to 10 images, GIFs and videos per message; a video or GIF over Discord's 20 MB attachment limit is sent as a link in the text. No Stories. Once sent, the post's `discord.result` carries the message id and the link. Discord gives webhooks no view counts: analytics carry the reactions and the poll votes.
187
+
188
+ ```ruby
189
+ client.posts.create(
190
+ content: "Patch 2.4 is out. Read the notes below.",
191
+ channels: ["discord"],
192
+ scheduled_at: "2026-11-02T09:00:00Z",
193
+ media_urls: ["https://example.com/patch-notes.png"],
194
+ discord: {
195
+ "silent" => true,
196
+ "buttons" => [{ "text" => "Patch notes", "url" => "https://example.com/patch-2-4" }]
197
+ }
198
+ )
199
+
200
+ # A native poll: the text is the question.
201
+ client.posts.create(
202
+ content: "Which mode do we test next?",
203
+ channels: ["discord"],
204
+ discord: { "poll" => { "options" => %w[Ranked Co-op Arena], "duration_hours" => 48 } }
205
+ )
206
+ ```
207
+
180
208
  ### Chained threads (X, Bluesky, Mastodon, Threads)
181
209
 
182
210
  Pass 2 to 25 `thread_parts` to publish a chained thread instead of a single tweet (each part is at most 280 characters, 25,000 for X Premium/Premium+ accounts). Bluesky, Mastodon and Threads support the same `thread_parts` shape (Threads: 2 to 25 parts, 500 characters per part, up to 10 media per part; parts after the first publish as replies to the previous part, and the Threads caption is taken from part 1):
@@ -216,6 +244,8 @@ On update, passing `x: { "thread_parts" => nil }` clears the thread and reverts
216
244
 
217
245
  Threads posts can also carry a location tag: pass `threads: { "location_id" => "..." }` with an id from `client.locations.search(platform: "threads")` (see Locations below). On a multi-post thread the tag is applied to part 1, and on update `threads: { "location_id" => nil }` clears it. Threads location tagging is currently rolling out; until Meta approves the permissions it is disabled on production and calls return a clear error.
218
246
 
247
+ Threads posts can carry a topic too: pass `threads: { "topic_tag" => "Build in public" }`. Unlike a #tag in the text, a topic can hold spaces. 1 to 50 characters, no period and no ampersand; a leading `#` is dropped. On a multi-post thread the topic is applied to part 1, and on update `threads: { "topic_tag" => nil }` removes it.
248
+
219
249
  ### X link posts use credits
220
250
 
221
251
  X bills API posts whose text contains a URL at a premium, and OmniSocials passes that fee through as prepaid credits (20 credits per URL-containing tweet; threads billed per part with a link). When a create targets X and the text contains a URL, the response Hash includes a top-level `"warnings"` array (a sibling of `"data"`):
@@ -395,6 +425,37 @@ client.posts.create(
395
425
  )
396
426
  ```
397
427
 
428
+ ## Post tags
429
+
430
+ Tags are your workspace's own labels for posts (a hook style, a format, a campaign), to filter posts and compare how each kind does. They never go to a platform. Uses the `posts:read` / `posts:write` scopes; the comparison uses `analytics:read`. A tag is `{ "id", "name", "color", "created_at" }`; names are unique in the workspace (case-insensitive, max 40 characters), at most 200 tags per workspace.
431
+
432
+ ```ruby
433
+ tag = client.tags.create(name: "Question", color: "teal") # color is optional
434
+ client.tags.list
435
+ client.tags.update(tag["data"]["id"], name: "Question hook")
436
+ client.tags.delete(tag["data"]["id"]) # { "data" => { "tag" => ..., "posts_untagged" => 4 } }
437
+
438
+ # Tag a post: ids or names, a new name becomes a tag (at most 20 per post)
439
+ client.posts.create(content: "Would you post this?", channels: ["instagram"], tags: ["Question"])
440
+ client.posts.update("1024", tags: ["Question", "Talking head"]) # replaces; tags: nil or [] removes all
441
+
442
+ # Up to 500 posts at once, published ones included: add, remove or set (replace)
443
+ result = client.tags.apply(post_ids: ["1024", "1025"], add: ["Question"], remove: ["Listicle"])
444
+ puts result["data"]["updated"], result["data"]["not_found"].inspect
445
+
446
+ # Filter by tag (an id or name, or an Array: posts with any of them)
447
+ client.posts.list(tag: "Question")
448
+ client.analytics.overview(period: "30d", tag: ["Question", "Talking head"])
449
+
450
+ # How each tag did against your normal (default: the last 90 days)
451
+ compare = client.analytics.tags(start_date: "2026-08-01", end_date: "2026-10-31")
452
+ compare["data"]["tags"].each do |row|
453
+ puts "#{row["tag"]["name"]} #{row["vs_normal"]} #{row["enough_posts"]}" # vs_normal 0.38 = 38% above
454
+ end
455
+ ```
456
+
457
+ `analytics.tags` returns `period`, `platform`, `min_posts`, `tags` (comparable tags first, best first; each with `posts`, `measured`, `median_impressions`, `engagement_rate` in percent, `vs_normal` as a fraction, `best_channel` and `enough_posts`), `untagged` (the same numbers for posts without a tag) and `normals` (median impressions per channel). `platform:` keeps one channel's numbers and `min_posts:` (default 5) sets how many measured posts a tag needs before `enough_posts` is true. Every post carries `tags` and `trial_reel` (`{ "strategy" => "MANUAL" | "SS_PERFORMANCE" }` for an Instagram Trial Reel, else `nil`), and so does every entry of `analytics.posts`. Errors: `404 tag_not_found`, `409 tag_exists` (the existing tag is in `e.body["error"]["tag"]`), `409 tag_limit_reached`, `400 invalid_tag_name`, `invalid_tag_color`, `invalid_tags`, `invalid_post_ids`, `too_many_posts`, `too_many_tags`.
458
+
398
459
  ## Accounts
399
460
 
400
461
  ```ruby
@@ -515,9 +576,15 @@ message_id = messages["data"][0]["id"]
515
576
  client.inbox.hide(message_id) # hide
516
577
  client.inbox.hide(message_id, hide: false) # unhide
517
578
  client.inbox.delete_message(message_id)
579
+
580
+ # Like a comment as the Page or account (Facebook, LinkedIn Page, TikTok).
581
+ # The returned message carries its "liked" flag.
582
+ liked = client.inbox.like(message_id) # like
583
+ client.inbox.like(message_id, like: false) # unlike
584
+ puts liked["data"]["liked"] # true
518
585
  ```
519
586
 
520
- `platform` accepts `"instagram"`, `"facebook"`, `"linkedin"`, `"tiktok"`, `"youtube"`, `"x"`, or `"threads"`; `type` accepts `"dm"`, `"comment"`, or `"mention"`. Threads conversations are comments (replies people leave on your Threads posts) and mentions; there are no Threads DMs. Comments can be hidden on Facebook, Instagram, TikTok, YouTube, and Threads (Threads: incoming top-level replies only), and a hidden message keeps its place in the conversation with its `hidden` flag set; `hidden` is `true`/`false` on comments and `nil` on DMs. A comment/mention's `post` carries `url` (public link when the platform provides one) and `media_type` (the platform's own label) next to `id`, `caption`, and `thumbnail`. The Threads inbox needs a Threads connection with the reply permission (a 401 `reauth_required` means reconnect Threads; an account connected before 2026-09-14 needs this once). TikTok and YouTube replies are comments only; TikTok replies are capped at 150 characters. Conversation ids are URL-encoded for you, so pass them exactly as returned - LinkedIn ids contain `":"` and `"()"` (e.g. `"linkedin_comment_urn:li:activity:123"`).
587
+ `platform` accepts `"instagram"`, `"facebook"`, `"linkedin"`, `"tiktok"`, `"youtube"`, `"x"`, or `"threads"`; `type` accepts `"dm"`, `"comment"`, or `"mention"`. Threads conversations are comments (replies people leave on your Threads posts) and mentions; there are no Threads DMs. Comments can be hidden on Facebook, Instagram, TikTok, YouTube, and Threads (Threads: incoming top-level replies only), and a hidden message keeps its place in the conversation with its `hidden` flag set; `hidden` is `true`/`false` on comments and `nil` on DMs. `liked` is `true`/`false` on Facebook, LinkedIn Page, and TikTok comments (whether the connected account liked it; on Facebook and LinkedIn it reflects likes made through OmniSocials, TikTok also syncs likes made in the TikTok app) and `nil` where the platform has no like through OmniSocials (DMs, Instagram, YouTube, X, Threads). A comment/mention's `post` carries `url` (public link when the platform provides one) and `media_type` (the platform's own label) next to `id`, `caption`, and `thumbnail`. The Threads inbox needs a Threads connection with the reply permission (a 401 `reauth_required` means reconnect Threads; an account connected before 2026-09-14 needs this once). TikTok and YouTube replies are comments only; TikTok replies are capped at 150 characters. Conversation ids are URL-encoded for you, so pass them exactly as returned - LinkedIn ids contain `":"` and `"()"` (e.g. `"linkedin_comment_urn:li:activity:123"`).
521
588
 
522
589
  `next` hands out the next conversation that still needs a reply (the customer's latest DM with no reply after it, or an unreplied comment/mention that is not hidden), with the whole thread and the post it belongs to, so a reply can be drafted from one call. DMs that can still be answered come first, then Instagram/Facebook DMs whose 24-hour window has closed (`reply_window["open"]` is `false`: answer those from the native app or mark them read), then comments and mentions, oldest first. Replies typed in the native apps count as answers. Only unread items are served by default, so `mark_read` is the durable way to skip one; `exclude` skips conversation ids for the current session only. Always pass `message_id: message["id"]` to `reply` on comment threads: every comment on a post shares one conversation, and without it the reply goes under the newest comment on the post. Pass `include_next: true` to `reply` to get the following item in the same response. `list_conversations(unanswered: true)` gives the same set as a plain list.
523
590
 
@@ -555,7 +622,7 @@ rescue OmniSocials::APIError => e
555
622
  when "insufficient_credits"
556
623
  puts "Not enough credits to send this reply (need 2)."
557
624
  when "x_inbox_suspended"
558
- puts "X inbox is suspended - top up and re-enable it in the dashboard."
625
+ puts "X inbox is suspended until a top-up."
559
626
  else
560
627
  raise
561
628
  end
@@ -61,7 +61,7 @@ module OmniSocials
61
61
  attr_reader :api_key, :base_url, :timeout, :max_retries,
62
62
  :posts, :media, :folders, :hashtag_sets, :approval_workflows,
63
63
  :accounts, :analytics, :audio, :locations, :pinterest,
64
- :automations, :inbox, :webhooks
64
+ :automations, :inbox, :webhooks, :tags
65
65
 
66
66
  # api_key - API key (omsk_live_*). Falls back to the
67
67
  # OMNISOCIALS_API_KEY environment variable. Raises
@@ -90,6 +90,7 @@ module OmniSocials
90
90
  @media = Resources::Media.new(self)
91
91
  @folders = Resources::Folders.new(self)
92
92
  @hashtag_sets = Resources::HashtagSets.new(self)
93
+ @tags = Resources::Tags.new(self)
93
94
  @approval_workflows = Resources::ApprovalWorkflows.new(self)
94
95
  @accounts = Resources::Accounts.new(self)
95
96
  @analytics = Resources::Analytics.new(self)
@@ -15,7 +15,9 @@ module OmniSocials
15
15
 
16
16
  # GET /analytics/posts?ids=a,b,c - bulk metrics for up to 100 posts in
17
17
  # one request. `ids` is an Array of post ids (or an already-joined
18
- # comma-separated String).
18
+ # comma-separated String). Each entry also carries "tags" (the post's
19
+ # tags) and "trial_reel" ({ "strategy" => "MANUAL" | "SS_PERFORMANCE" }
20
+ # for an Instagram Trial Reel, else nil).
19
21
  def posts(ids)
20
22
  @client.request(
21
23
  "GET", "/analytics/posts",
@@ -24,10 +26,17 @@ module OmniSocials
24
26
  end
25
27
 
26
28
  # GET /analytics/overview - workspace-wide analytics overview.
27
- def overview(period: nil, start_date: nil, end_date: nil)
29
+ #
30
+ # tag (a tag id or name, or an Array of them: any of them) keeps the
31
+ # post numbers of the posts with that tag; account numbers stay those
32
+ # of the whole account. An unknown tag is a 404 tag_not_found.
33
+ def overview(period: nil, start_date: nil, end_date: nil, tag: nil)
28
34
  @client.request(
29
35
  "GET", "/analytics/overview",
30
- query: { "period" => period, "start_date" => start_date, "end_date" => end_date }
36
+ query: {
37
+ "period" => period, "start_date" => start_date, "end_date" => end_date,
38
+ "tag" => Tags.tag_query(tag)
39
+ }
31
40
  )
32
41
  end
33
42
 
@@ -49,6 +58,28 @@ module OmniSocials
49
58
  query: { "platform" => platform, "timezone" => timezone }
50
59
  )
51
60
  end
61
+
62
+ # GET /analytics/tags - how each tag did against your normal, for the
63
+ # published posts in a period (default the last 90 days).
64
+ #
65
+ # Returns { "data" => { "period", "platform", "min_posts", "tags",
66
+ # "untagged", "normals" } }. Each entry of "tags" is { "tag", "posts",
67
+ # "measured", "median_impressions", "engagement_rate" (percent),
68
+ # "vs_normal" (fraction: 0.38 = 38% above your normal), "best_channel",
69
+ # "enough_posts" }, comparable tags first, best first; "untagged" has
70
+ # the same numbers for the posts without a tag, "normals" the median
71
+ # impressions per channel. platform keeps one channel's numbers;
72
+ # min_posts (1 to 50, default 5) is the measured posts a tag needs
73
+ # before "enough_posts" is true.
74
+ def tags(start_date: nil, end_date: nil, platform: nil, min_posts: nil)
75
+ @client.request(
76
+ "GET", "/analytics/tags",
77
+ query: {
78
+ "start_date" => start_date, "end_date" => end_date,
79
+ "platform" => platform, "min_posts" => min_posts
80
+ }
81
+ )
82
+ end
52
83
  end
53
84
  end
54
85
  end
@@ -5,9 +5,15 @@ module OmniSocials
5
5
  # Automations resource: comment-to-DM automations (list, get, create,
6
6
  # update, delete, events). An automation answers a keyword in an
7
7
  # Instagram comment (or a DM, or a story reply) with a DM, once per
8
- # person, with an optional public reply under the comment. The scopes
9
- # are opt-in on the API key: automations:read for list / get /
10
- # list_events, automations:write for create / update / delete (and for
8
+ # person, with an optional public reply under the comment. With
9
+ # platform: "facebook" it runs on a Facebook Page instead, and the DM
10
+ # lands in Messenger (a Page has no story replies). With platform: "x"
11
+ # it answers a keyword in a DM to the X account ("dm" kind only, no
12
+ # post, no public reply) while X DMs are on for the workspace; every X
13
+ # answer ends with "Reply STOP to stop automatic messages." and a
14
+ # person who replies STOP gets no automatic DM again. The scopes are
15
+ # opt-in on the API key: automations:read for list / get / list_events,
16
+ # automations:write for create / update / delete (and for
11
17
  # comment_dm_automation on posts).
12
18
  class Automations
13
19
  def initialize(client)
@@ -15,13 +21,14 @@ module OmniSocials
15
21
  end
16
22
 
17
23
  # GET /automations - list the workspace's comment-to-DM automations,
18
- # optionally by status ("active", "paused", "completed") or by the
19
- # OmniSocials post_id they are bound to. Requires the automations:read
20
- # scope (opt-in on the API key).
21
- def list(status: nil, post_id: nil)
24
+ # optionally by status ("active", "paused", "completed"), by platform
25
+ # ("instagram", "facebook", "x") or by the OmniSocials post_id they are
26
+ # bound to. Requires the automations:read scope (opt-in on the API
27
+ # key).
28
+ def list(status: nil, post_id: nil, platform: nil)
22
29
  @client.request(
23
30
  "GET", "/automations",
24
- query: { "status" => status, "post_id" => post_id }
31
+ query: { "status" => status, "platform" => platform, "post_id" => post_id }
25
32
  )
26
33
  end
27
34
 
@@ -35,10 +42,21 @@ module OmniSocials
35
42
  #
36
43
  # A keyword in an Instagram comment (or a DM, or a story reply) is
37
44
  # answered with a DM, once per person, with an optional public reply
38
- # under the comment. Bind it to one post with post_id (an OmniSocials
39
- # post that goes to Instagram, scheduled or live; one automation per
40
- # post) or instagram_media_id (a post published outside OmniSocials),
41
- # not both; a trigger scope of "next_post" or "any" needs neither.
45
+ # under the comment. platform is "instagram" (default) or "facebook":
46
+ # on a Facebook Page a keyword in a comment (or a Messenger message to
47
+ # the Page) is answered in Messenger, and a Page has no story replies.
48
+ # platform: "x" answers a keyword in a DM to the X account: trigger
49
+ # kinds must be ["dm"] (X allows an automated DM only after the person
50
+ # wrote first), with no post and no public reply; an active X
51
+ # automation needs X DMs on for the workspace (409 x_dms_off; status
52
+ # "paused" always saves), and every X answer ends with "Reply STOP to
53
+ # stop automatic messages.". Bind it to one post with post_id (an OmniSocials post that goes to
54
+ # the channel, scheduled or live; one automation per post and
55
+ # platform), instagram_media_id (an Instagram post published outside
56
+ # OmniSocials) or facebook_post_id (a Page post published outside
57
+ # OmniSocials, as "{page-id}_{post-id}" or the post id alone; implies
58
+ # "facebook"), at most one; a trigger scope of "next_post" or "any"
59
+ # needs none.
42
60
  #
43
61
  # `actions` is required: { "dm" => { "text" => "..." } } (max 1000
44
62
  # characters) plus an optional "public_reply" => { "enabled" => true,
@@ -49,20 +67,27 @@ module OmniSocials
49
67
  # characters, required unless match is "any"; match is "word",
50
68
  # "contains" or "any". `limits` is { "max_sends" => 500, "stop_at" =>
51
69
  # "2026-12-31T00:00:00Z" } (both optional). `status` is "active"
52
- # (default) or "paused". Instagram rules: one private reply per
53
- # comment, within 7 days; OmniSocials caps sends at 180 DMs an hour per
54
- # account.
70
+ # (default) or "paused". Meta rules, the same on Instagram and
71
+ # Facebook: one private reply per comment, within 7 days; OmniSocials
72
+ # caps sends at 180 DMs an hour per account and platform, and on X at
73
+ # 15 per 15 minutes and 450 a day; an answer over a cap waits in a
74
+ # queue ("queued") for up to an hour.
55
75
  #
56
76
  # Requires the automations:write scope (opt-in on the API key).
57
77
  # Errors: 400 validation_error, 400 post_not_instagram, 400
58
- # post_required, 404 post_not_found, 404 instagram_media_not_found,
59
- # 409 instagram_not_connected, 409 instagram_needs_reauth, 409
60
- # automation_exists (the error body also carries automation_id).
78
+ # post_not_facebook, 400 post_required, 404 post_not_found, 404
79
+ # instagram_media_not_found, 404 facebook_post_not_found, 409
80
+ # instagram_not_connected, 409 instagram_needs_reauth, 409
81
+ # facebook_not_connected, 409 facebook_needs_reauth, 409
82
+ # x_not_connected, 409 x_dms_off, 409 automation_exists (the error
83
+ # body also carries automation_id).
61
84
  def create(actions:, trigger: nil, limits: nil, name: nil, post_id: nil,
62
- instagram_media_id: nil, status: nil)
85
+ instagram_media_id: nil, status: nil, platform: nil,
86
+ facebook_post_id: nil)
63
87
  body = automation_body(
64
88
  name: name, post_id: post_id, instagram_media_id: instagram_media_id,
65
- trigger: trigger, actions: actions, limits: limits, status: status
89
+ trigger: trigger, actions: actions, limits: limits, status: status,
90
+ platform: platform, facebook_post_id: facebook_post_id
66
91
  )
67
92
  @client.request("POST", "/automations", json: body)
68
93
  end
@@ -76,15 +101,20 @@ module OmniSocials
76
101
  # "public_reply" => { "enabled" => false } or { "variations" => [] }
77
102
  # turns the public reply off; limits: { "max_sends" => nil } removes
78
103
  # that limit. status: "paused" pauses and "active" resumes; another
79
- # post_id / instagram_media_id moves the automation to that post. A
104
+ # post_id / instagram_media_id / facebook_post_id moves the automation
105
+ # to that post. The platform cannot change: a platform other than the
106
+ # automation's is a 400 platform_immutable. status: "active" on an X
107
+ # automation needs X DMs on for the workspace (409 x_dms_off). A
80
108
  # "completed" automation starts again when its limits or trigger
81
- # change. Requires the automations:write scope. 400 no_fields when
82
- # nothing is passed.
109
+ # change. Requires the
110
+ # automations:write scope. 400 no_fields when nothing is passed.
83
111
  def update(automation_id, name: nil, post_id: nil, instagram_media_id: nil,
84
- trigger: nil, actions: nil, limits: nil, status: nil)
112
+ trigger: nil, actions: nil, limits: nil, status: nil,
113
+ platform: nil, facebook_post_id: nil)
85
114
  body = automation_body(
86
115
  name: name, post_id: post_id, instagram_media_id: instagram_media_id,
87
- trigger: trigger, actions: actions, limits: limits, status: status
116
+ trigger: trigger, actions: actions, limits: limits, status: status,
117
+ platform: platform, facebook_post_id: facebook_post_id
88
118
  )
89
119
  @client.request("PATCH", "/automations/#{automation_id}", json: body)
90
120
  end
@@ -97,15 +127,20 @@ module OmniSocials
97
127
 
98
128
  # GET /automations/{id}/events - the automation's activity: one event
99
129
  # per person it answered, skipped or failed on. outcome filters on
100
- # "sent", "skipped", "failed" or "pending"; limit is 1..100 (default
101
- # 25), offset skips. Requires the automations:read scope.
130
+ # "sent", "skipped", "failed", "pending", "queued" (waits for a free
131
+ # slot under the send cap, out within the hour) or "opted_out" (the
132
+ # person replied STOP on X); limit is 1..100 (default 25), offset
133
+ # skips. Requires the automations:read scope.
102
134
  #
103
135
  # The response is not the plain list envelope: { "data" => [...],
104
136
  # "counts" => { "sent" => 12, "skipped" => 3 }, "pagination" => {...} },
105
137
  # where counts is the number of events per outcome (an outcome with no
106
138
  # events is left out). Each event carries kind ("comment", "dm",
107
- # "story_reply"), outcome, reason / reason_label, person (id,
108
- # username), trigger_id, trigger_text, message (the DM sent),
139
+ # "story_reply"), outcome, reason / reason_label (X adds the skip
140
+ # reasons "dms_closed", "no_credits", "opted_out" and
141
+ # "automation_off"), person (id,
142
+ # username; on Facebook the person's name, since Facebook has no
143
+ # @handle), trigger_id, trigger_text, message (the DM sent),
109
144
  # public_reply, platform_message_id, created_at and sent_at.
110
145
  def list_events(automation_id, outcome: nil, limit: nil, offset: nil)
111
146
  @client.request(
@@ -117,12 +152,15 @@ module OmniSocials
117
152
  private
118
153
 
119
154
  def automation_body(name:, post_id:, instagram_media_id:, trigger:,
120
- actions:, limits:, status:)
155
+ actions:, limits:, status:, platform: nil,
156
+ facebook_post_id: nil)
121
157
  Internal.drop_nil(
122
158
  {
159
+ "platform" => platform,
123
160
  "name" => name,
124
161
  "post_id" => post_id,
125
162
  "instagram_media_id" => instagram_media_id,
163
+ "facebook_post_id" => facebook_post_id,
126
164
  "trigger" => trigger,
127
165
  "actions" => actions,
128
166
  "limits" => limits,
@@ -167,6 +167,32 @@ module OmniSocials
167
167
  )
168
168
  end
169
169
 
170
+ # POST /inbox/messages/{id}/like - like or unlike a comment someone left
171
+ # on one of the user's posts, as the connected Page or account.
172
+ # Facebook Page, LinkedIn Page and TikTok comments (incoming comments
173
+ # only). Pass like: false to remove the like. Facebook allows only Like
174
+ # (no Love or other reactions). Liking a comment that is already liked,
175
+ # or unliking one that is not, succeeds. Returns the comment after the
176
+ # change with its "liked" flag set: true when the account liked it,
177
+ # false when not, nil where the platform has no like through
178
+ # OmniSocials (DMs, Instagram, YouTube, X, Threads). On Facebook and
179
+ # LinkedIn "liked" reflects likes made through OmniSocials; TikTok also
180
+ # syncs likes made in the TikTok app. Requires the inbox:write scope.
181
+ #
182
+ # Errors: 400 "unsupported_platform" (not an incoming Facebook, LinkedIn
183
+ # or TikTok comment), 401 "reauth_required" (the TikTok comments
184
+ # authorization is missing or expired, or the LinkedIn Page connection expired), 403 "reconnect_required" (the
185
+ # account was connected without the permission to like comments;
186
+ # reconnect it in the dashboard), 404 "not_found" (message not in this
187
+ # workspace) or "account_not_connected", 502 "platform_error" (the
188
+ # platform rejected the call).
189
+ def like(message_id, like: true)
190
+ @client.request(
191
+ "POST", "/inbox/messages/#{encode_id(message_id)}/like",
192
+ json: { "like" => like }
193
+ )
194
+ end
195
+
170
196
  # DELETE /inbox/messages/{id} - delete a comment someone left on one of
171
197
  # the user's posts, on the platform and from the inbox. Facebook,
172
198
  # Instagram and TikTok comments only: YouTube's API does not let a
@@ -218,9 +244,9 @@ module OmniSocials
218
244
  # { "conversation", "message", "messages", "reply_window" }, or nil
219
245
  # when nothing is waiting. "message" is the unanswered incoming item
220
246
  # itself (the customer's latest DM, or the specific comment): its "id"
221
- # is what `hide` and `delete_message` take and the message_id: to pass
222
- # to `reply` on comment threads, its "conversation_id" is what `reply`
223
- # takes. "messages" is the conversation so far, oldest first (the most
247
+ # is what `hide`, `like` and `delete_message` take and the message_id:
248
+ # to pass to `reply` on comment threads, its "conversation_id" is what
249
+ # `reply` takes. "messages" is the conversation so far, oldest first (the most
224
250
  # recent 50 messages for long DM threads). "reply_window" is
225
251
  # { "open" => Boolean, "closes_at" => String|nil }: "open" is false
226
252
  # only for an Instagram/Facebook DM past its 24-hour window, which
@@ -21,10 +21,17 @@ module OmniSocials
21
21
  # GET /posts - list posts (status: draft, in_approval, scheduled, posting,
22
22
  # posted, failed, warning; in_approval = waiting for a reviewer in an
23
23
  # approval workflow).
24
- def list(status: nil, limit: nil, offset: nil)
24
+ # tag (a tag id or name, or an Array of them) keeps the posts with any
25
+ # of those tags; an unknown tag is a 404 tag_not_found. Every post
26
+ # carries "tags" (its tags) and "trial_reel" ({ "strategy" =>
27
+ # "MANUAL" | "SS_PERFORMANCE" } for an Instagram Trial Reel, else nil).
28
+ def list(status: nil, limit: nil, offset: nil, tag: nil)
25
29
  @client.request(
26
30
  "GET", "/posts",
27
- query: { "status" => status, "limit" => limit, "offset" => offset }
31
+ query: {
32
+ "status" => status, "limit" => limit, "offset" => offset,
33
+ "tag" => Tags.tag_query(tag)
34
+ }
28
35
  )
29
36
  end
30
37
 
@@ -106,6 +113,11 @@ module OmniSocials
106
113
  # 400 validation_error asking you to reconnect Threads when the
107
114
  # connection lacks the threads_location_tagging permission).
108
115
  #
116
+ # Threads posts can carry a topic: threads: { "topic_tag" => "Build in
117
+ # public" } (1 to 50 characters, no period and no ampersand; a topic can
118
+ # hold spaces). On a multi-post thread it goes on part 1; on update
119
+ # "topic_tag" => nil removes it.
120
+ #
109
121
  # Pinterest posts can tag products: pass
110
122
  # pinterest: { "board_id" => "...", "product_tags" => [...] } with up
111
123
  # to 24 product Pins of the connected Pinterest account, each as a Pin
@@ -129,19 +141,46 @@ module OmniSocials
129
141
  # stored options. Once sent, the Post's "telegram" block carries
130
142
  # "result" (message ids and links).
131
143
  #
144
+ # discord holds the Discord options (one Discord channel, posted
145
+ # through its incoming webhook with the webhook's name and picture):
146
+ # "silent", "link_preview" (default on), "allow_mentions" (default off:
147
+ # @everyone, @here, role and user mentions show as text and ping
148
+ # nobody), "spoiler" (booleans), "thread_name" (the post title in a
149
+ # forum or media channel, 1 to 100 characters; on a text channel the
150
+ # post goes out without it), up to 5 "buttons" ({ "text" => 1 to 80 characters, "url" =>
151
+ # https:// or http:// }, one row, not with a poll) and a native "poll"
152
+ # ({ "options" => 2 to 10 of up to 55 characters, "duration_hours" =>
153
+ # 1 to 768 (default 24), "multiple" => true }; the post text is the
154
+ # question, max 300 characters, no media and no buttons). Text up to
155
+ # 2,000 characters; up to 10 images, GIFs and videos per message, a
156
+ # video or GIF over 20 MB is sent as a link in the text. No Stories.
157
+ # On #update every key merges into the stored options, a key set to
158
+ # nil removes it and discord: nil removes every option. Once sent, the
159
+ # Post's "discord" block carries "result" (message id and link).
160
+ #
132
161
  # 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
162
+ # (Instagram or Facebook must be in channels): a keyword in a comment
163
+ # is answered with a DM, once per person, with an optional public
164
+ # reply under the comment. Shape: { "trigger" => { "keywords" =>
165
+ # ["guide"], "match" => "word" }, "actions" => { "dm" => { "text" =>
166
+ # "..." }, "public_reply" => { "enabled" => true, "variations" =>
167
+ # [...] } }, "limits" => { "max_sends" => 500 }, "platforms" =>
168
+ # ["instagram", "facebook"] } (see automations.create for the
169
+ # fields). "platforms" makes one automation per listed channel; each
170
+ # must be a channel of the post. Without it: Instagram when the post
171
+ # goes there, else Facebook. Needs the automations:write scope on the
172
+ # key (403 insufficient_scope, checked before anything else); a bad
173
+ # Hash is a 400 validation_error whose message starts with
142
174
  # "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.
175
+ # data["comment_dm_automation"] (the first automation, with "post"
176
+ # nil, plus "platforms" and "automations", one { "id", "platform",
177
+ # "status" } per channel); each starts listening once the post is
178
+ # live on its channel.
179
+ #
180
+ # tags labels the post with the workspace's own tags (ids or names, at
181
+ # most 20; a name that does not exist yet becomes a new tag) to filter
182
+ # and compare posts in Analytics. Tags never go to a platform. Errors:
183
+ # 404 tag_not_found (an unknown id), 400 invalid_tags / too_many_tags.
145
184
  def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
146
185
  media_urls: nil, type: nil, source: nil, link_url: nil,
147
186
  link_title: nil, link_description: nil, link_thumbnail_url: nil,
@@ -151,9 +190,9 @@ module OmniSocials
151
190
  instagram: nil, facebook: nil, linkedin: nil,
152
191
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
153
192
  mastodon: nil, threads: nil, google_business: nil,
154
- telegram: nil, linkedin_poll: nil,
193
+ telegram: nil, discord: nil, linkedin_poll: nil,
155
194
  approval_workflow_id: nil, draft: nil, media_fit: nil,
156
- comment_dm_automation: nil)
195
+ comment_dm_automation: nil, tags: nil)
157
196
  body = create_body(
158
197
  content: content, channels: channels, scheduled_at: scheduled_at,
159
198
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -167,9 +206,10 @@ module OmniSocials
167
206
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
168
207
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
169
208
  google_business: google_business, telegram: telegram,
170
- linkedin_poll: linkedin_poll,
209
+ discord: discord, linkedin_poll: linkedin_poll,
171
210
  approval_workflow_id: approval_workflow_id, draft: draft,
172
- media_fit: media_fit, comment_dm_automation: comment_dm_automation
211
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation,
212
+ tags: tags
173
213
  )
174
214
  @client.request("POST", "/posts/create", json: body)
175
215
  end
@@ -177,8 +217,8 @@ module OmniSocials
177
217
  # POST /posts/create-and-publish - create and publish immediately.
178
218
  # See #create for the "warnings" array (X link posts, and the
179
219
  # "image_fit" entries steered by media_fit), the 402
180
- # "x_credits_insufficient" credit gate on X link posts and
181
- # comment_dm_automation.
220
+ # "x_credits_insufficient" credit gate on X link posts,
221
+ # comment_dm_automation and tags.
182
222
  def create_and_publish(content:, channels: nil, media_ids: nil,
183
223
  media_urls: nil, type: nil, source: nil,
184
224
  link_url: nil, link_title: nil, link_description: nil,
@@ -190,8 +230,9 @@ module OmniSocials
190
230
  facebook: nil, linkedin: nil, linkedin_page: nil,
191
231
  tiktok: nil, x: nil, bluesky: nil, mastodon: nil,
192
232
  threads: nil, google_business: nil,
193
- telegram: nil, linkedin_poll: nil, media_fit: nil,
194
- comment_dm_automation: nil)
233
+ telegram: nil, discord: nil, linkedin_poll: nil,
234
+ media_fit: nil,
235
+ comment_dm_automation: nil, tags: nil)
195
236
  body = create_body(
196
237
  content: content, channels: channels, scheduled_at: nil,
197
238
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -205,8 +246,9 @@ module OmniSocials
205
246
  linkedin: linkedin, linkedin_page: linkedin_page, tiktok: tiktok,
206
247
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
207
248
  google_business: google_business, telegram: telegram,
208
- linkedin_poll: linkedin_poll,
209
- media_fit: media_fit, comment_dm_automation: comment_dm_automation
249
+ discord: discord, linkedin_poll: linkedin_poll,
250
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation,
251
+ tags: tags
210
252
  )
211
253
  @client.request("POST", "/posts/create-and-publish", json: body)
212
254
  end
@@ -240,17 +282,28 @@ module OmniSocials
240
282
  # can also refuse an update to a scheduled X link post.
241
283
  #
242
284
  # 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.
285
+ # the post's comment-to-DM automations (a channel no longer in
286
+ # "platforms" loses its automation), an explicit nil removes them all,
287
+ # and leaving it out keeps them. Needs the automations:write scope.
288
+ #
289
+ # discord merges into the stored Discord options: a key set to nil
290
+ # removes that option (e.g. discord: { "poll" => nil }), an explicit
291
+ # discord: nil removes every option, and leaving it out keeps them.
292
+ #
293
+ # tags (ids or names, a new name becomes a tag) replaces the post's
294
+ # tags; tags: nil or tags: [] removes them all, and leaving it out
295
+ # keeps them. An update with only tags works on every post, published
296
+ # and failed ones included.
245
297
  def update(post_id, content: nil, scheduled_at: nil, channels: nil,
246
298
  media_ids: nil, media_urls: nil, type: nil, location_id: nil,
247
299
  collaborators: nil, user_tags: nil, pinterest: nil,
248
300
  youtube: nil, instagram: nil, facebook: nil, linkedin: nil,
249
301
  linkedin_page: nil, tiktok: nil, x: nil, bluesky: nil,
250
302
  mastodon: nil, threads: nil, google_business: nil,
251
- telegram: nil, linkedin_poll: nil, draft: nil,
252
- media_fit: nil,
253
- comment_dm_automation: OmniSocials::NOT_GIVEN)
303
+ telegram: nil, discord: OmniSocials::NOT_GIVEN,
304
+ linkedin_poll: nil, draft: nil, media_fit: nil,
305
+ comment_dm_automation: OmniSocials::NOT_GIVEN,
306
+ tags: OmniSocials::NOT_GIVEN)
254
307
  body = Internal.drop_nil(
255
308
  {
256
309
  "content" => content,
@@ -283,6 +336,10 @@ module OmniSocials
283
336
  # An explicit nil removes the post's automation, so it must survive
284
337
  # drop_nil; NOT_GIVEN (the default) leaves it untouched.
285
338
  body["comment_dm_automation"] = comment_dm_automation unless comment_dm_automation.is_a?(NotGiven)
339
+ # Same for discord: an explicit nil removes every Discord option.
340
+ body["discord"] = discord unless discord.is_a?(NotGiven)
341
+ # And for tags: an explicit nil (or []) removes every tag of the post.
342
+ body["tags"] = tags unless tags.is_a?(NotGiven)
286
343
  @client.request("PATCH", "/posts/#{post_id}", json: body)
287
344
  end
288
345
 
@@ -358,8 +415,9 @@ module OmniSocials
358
415
  pinterest:, youtube:, instagram:, facebook:, linkedin:,
359
416
  linkedin_page:, tiktok:, x:, bluesky:, mastodon:,
360
417
  threads:, google_business:, linkedin_poll:,
361
- telegram: nil, approval_workflow_id: nil, draft: nil,
362
- media_fit: nil, comment_dm_automation: nil)
418
+ telegram: nil, discord: nil, approval_workflow_id: nil,
419
+ draft: nil,
420
+ media_fit: nil, comment_dm_automation: nil, tags: nil)
363
421
  Internal.drop_nil(
364
422
  {
365
423
  "content" => content,
@@ -393,11 +451,13 @@ module OmniSocials
393
451
  "threads" => threads,
394
452
  "google_business" => google_business,
395
453
  "telegram" => telegram,
454
+ "discord" => discord,
396
455
  "linkedin_poll" => linkedin_poll,
397
456
  "approval_workflow_id" => approval_workflow_id,
398
457
  "draft" => draft,
399
458
  "media_fit" => media_fit,
400
- "comment_dm_automation" => comment_dm_automation
459
+ "comment_dm_automation" => comment_dm_automation,
460
+ "tags" => tags
401
461
  }
402
462
  )
403
463
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniSocials
4
+ module Resources
5
+ # Tags resource: the workspace's own labels for its posts (a hook style,
6
+ # a format), to filter and compare posts in Analytics. Tags never go to a
7
+ # platform.
8
+ #
9
+ # Set them on a post with tags: on posts.create / create_and_publish /
10
+ # update, in bulk with #apply, filter with tag: on posts.list and
11
+ # analytics.overview, and compare them with analytics.tags. A tag
12
+ # reference is a tag id (Integer or String) or a tag name
13
+ # (case-insensitive); in add / set (and tags: on posts) a name that does
14
+ # not exist yet becomes a new tag.
15
+ class Tags
16
+ # Serialize the tag filter: one id or name, or an Array joined with ",".
17
+ def self.tag_query(tag)
18
+ return nil if tag.nil?
19
+ return tag.map(&:to_s).join(",") if tag.is_a?(Array)
20
+
21
+ tag.to_s
22
+ end
23
+
24
+ def initialize(client)
25
+ @client = client
26
+ end
27
+
28
+ # GET /tags - every tag of the workspace, by name. Each tag is
29
+ # { "id", "name", "color", "created_at" }. Scope posts:read.
30
+ def list
31
+ @client.request("GET", "/tags")
32
+ end
33
+
34
+ # POST /tags - create a tag. `name` is at most 40 characters and unique
35
+ # in the workspace (case-insensitive); `color` is one of blue, green,
36
+ # orange, red, purple, pink, teal, yellow, brown, gray (omit it to get
37
+ # the next colour). At most 200 tags per workspace.
38
+ #
39
+ # Errors: 409 tag_exists (the existing tag is in
40
+ # error.body["error"]["tag"]), 409 tag_limit_reached,
41
+ # 400 invalid_tag_name / invalid_tag_color.
42
+ def create(name:, color: nil)
43
+ @client.request(
44
+ "POST", "/tags",
45
+ json: Internal.drop_nil({ "name" => name, "color" => color })
46
+ )
47
+ end
48
+
49
+ # PATCH /tags/{id} - rename or recolour a tag. Only the keys you pass
50
+ # are sent; posts keep the tag. Errors: 404 tag_not_found,
51
+ # 409 tag_exists.
52
+ def update(tag_id, name: nil, color: nil)
53
+ body = Internal.drop_nil({ "name" => name, "color" => color })
54
+ @client.request("PATCH", "/tags/#{tag_id}", json: body)
55
+ end
56
+
57
+ # DELETE /tags/{id} - delete a tag and take it off every post (the
58
+ # posts stay). Returns { "data" => { "tag", "posts_untagged" } }.
59
+ def delete(tag_id)
60
+ @client.request("DELETE", "/tags/#{tag_id}")
61
+ end
62
+
63
+ # POST /tags/apply - add, remove or replace the tags of up to 500 posts
64
+ # at once, published ones included.
65
+ #
66
+ # add, remove and set take tag ids or names; a new name in add or set
67
+ # becomes a tag. set replaces a post's tags (set: [] removes them all),
68
+ # add and remove change only the tags you name. Returns
69
+ # { "data" => { "updated", "posts" => [{ "id", "tags" }], "not_found" } };
70
+ # not_found lists the post ids that are not posts of this workspace.
71
+ # Errors: 400 invalid_post_ids / too_many_posts / too_many_tags /
72
+ # invalid_tags, 404 tag_not_found.
73
+ def apply(post_ids:, add: nil, remove: nil, set: nil)
74
+ body = Internal.drop_nil(
75
+ {
76
+ "post_ids" => Array(post_ids).map(&:to_s),
77
+ "add" => add,
78
+ "remove" => remove,
79
+ "set" => set
80
+ }
81
+ )
82
+ @client.request("POST", "/tags/apply", json: body)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmniSocials
4
- VERSION = "0.11.0"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/omnisocials.rb CHANGED
@@ -20,6 +20,7 @@ require_relative "omnisocials/resources/posts"
20
20
  require_relative "omnisocials/resources/media"
21
21
  require_relative "omnisocials/resources/folders"
22
22
  require_relative "omnisocials/resources/hashtag_sets"
23
+ require_relative "omnisocials/resources/tags"
23
24
  require_relative "omnisocials/resources/approval_workflows"
24
25
  require_relative "omnisocials/resources/accounts"
25
26
  require_relative "omnisocials/resources/analytics"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnisocials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OmniSocials
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-21 00:00:00.000000000 Z
11
+ date: 2026-11-02 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,
@@ -37,6 +37,7 @@ files:
37
37
  - lib/omnisocials/resources/media.rb
38
38
  - lib/omnisocials/resources/pinterest.rb
39
39
  - lib/omnisocials/resources/posts.rb
40
+ - lib/omnisocials/resources/tags.rb
40
41
  - lib/omnisocials/resources/webhooks.rb
41
42
  - lib/omnisocials/version.rb
42
43
  - lib/omnisocials/webhooks.rb