omnisocials 0.13.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: 712ed8026c6e6223f5d2bbcaaa0696cda2ed181ab8f4da6bd2b95fd2430bc84e
4
- data.tar.gz: 2e1cf587a451e8f966800ddef4f74b1b8a0422e0df7fd7ffb078afdf5e1636eb
3
+ metadata.gz: 27468385685af2b1f49e6665393bb687f2dd9fc18cf7d06b98190c78c9b3c3fe
4
+ data.tar.gz: 24dafc551dc62fd9922aa9ce330ae138288a3ecd86dc768abcf35e2f5f466cae
5
5
  SHA512:
6
- metadata.gz: f45800a5a3a36c56e37df9085ee31c95182cfaec1f1a30224a65a82d538884f66c40bbd677b48a47236dd92dcc41e81db6e869c066d10f4b360debb566eb1d80
7
- data.tar.gz: 48569b613c4247848a3638727b65f8ba71ac1a9e1f1051c12843f02e738771536058290909d6623a97b5c1dc09d92eacf9c5c12c917196b5f7467917178e320e
6
+ metadata.gz: 43d61512382a55a5ef87c2e906f28f55153a15d3deed758262e6f919eeb934e5de86be8e700570ffc2bb9862a33e111e285085248ed0bbd7f3645046a3dcf6ce
7
+ data.tar.gz: 93c23762f52f0550bf38fcae55d850aafa24365966adad31d71b603c8c9fbee3c297be8cb31219d947fc2dd38d1d7b7b2252642460ae98d65a024060cb3d16ba
data/README.md CHANGED
@@ -425,6 +425,37 @@ client.posts.create(
425
425
  )
426
426
  ```
427
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
+
428
459
  ## Accounts
429
460
 
430
461
  ```ruby
@@ -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
@@ -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
 
@@ -169,6 +176,11 @@ module OmniSocials
169
176
  # nil, plus "platforms" and "automations", one { "id", "platform",
170
177
  # "status" } per channel); each starts listening once the post is
171
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.
172
184
  def create(content:, channels: nil, scheduled_at: nil, media_ids: nil,
173
185
  media_urls: nil, type: nil, source: nil, link_url: nil,
174
186
  link_title: nil, link_description: nil, link_thumbnail_url: nil,
@@ -180,7 +192,7 @@ module OmniSocials
180
192
  mastodon: nil, threads: nil, google_business: nil,
181
193
  telegram: nil, discord: nil, linkedin_poll: nil,
182
194
  approval_workflow_id: nil, draft: nil, media_fit: nil,
183
- comment_dm_automation: nil)
195
+ comment_dm_automation: nil, tags: nil)
184
196
  body = create_body(
185
197
  content: content, channels: channels, scheduled_at: scheduled_at,
186
198
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -196,7 +208,8 @@ module OmniSocials
196
208
  google_business: google_business, telegram: telegram,
197
209
  discord: discord, linkedin_poll: linkedin_poll,
198
210
  approval_workflow_id: approval_workflow_id, draft: draft,
199
- media_fit: media_fit, comment_dm_automation: comment_dm_automation
211
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation,
212
+ tags: tags
200
213
  )
201
214
  @client.request("POST", "/posts/create", json: body)
202
215
  end
@@ -204,8 +217,8 @@ module OmniSocials
204
217
  # POST /posts/create-and-publish - create and publish immediately.
205
218
  # See #create for the "warnings" array (X link posts, and the
206
219
  # "image_fit" entries steered by media_fit), the 402
207
- # "x_credits_insufficient" credit gate on X link posts and
208
- # comment_dm_automation.
220
+ # "x_credits_insufficient" credit gate on X link posts,
221
+ # comment_dm_automation and tags.
209
222
  def create_and_publish(content:, channels: nil, media_ids: nil,
210
223
  media_urls: nil, type: nil, source: nil,
211
224
  link_url: nil, link_title: nil, link_description: nil,
@@ -219,7 +232,7 @@ module OmniSocials
219
232
  threads: nil, google_business: nil,
220
233
  telegram: nil, discord: nil, linkedin_poll: nil,
221
234
  media_fit: nil,
222
- comment_dm_automation: nil)
235
+ comment_dm_automation: nil, tags: nil)
223
236
  body = create_body(
224
237
  content: content, channels: channels, scheduled_at: nil,
225
238
  media_ids: media_ids, media_urls: media_urls, type: type,
@@ -234,7 +247,8 @@ module OmniSocials
234
247
  x: x, bluesky: bluesky, mastodon: mastodon, threads: threads,
235
248
  google_business: google_business, telegram: telegram,
236
249
  discord: discord, linkedin_poll: linkedin_poll,
237
- media_fit: media_fit, comment_dm_automation: comment_dm_automation
250
+ media_fit: media_fit, comment_dm_automation: comment_dm_automation,
251
+ tags: tags
238
252
  )
239
253
  @client.request("POST", "/posts/create-and-publish", json: body)
240
254
  end
@@ -275,6 +289,11 @@ module OmniSocials
275
289
  # discord merges into the stored Discord options: a key set to nil
276
290
  # removes that option (e.g. discord: { "poll" => nil }), an explicit
277
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.
278
297
  def update(post_id, content: nil, scheduled_at: nil, channels: nil,
279
298
  media_ids: nil, media_urls: nil, type: nil, location_id: nil,
280
299
  collaborators: nil, user_tags: nil, pinterest: nil,
@@ -283,7 +302,8 @@ module OmniSocials
283
302
  mastodon: nil, threads: nil, google_business: nil,
284
303
  telegram: nil, discord: OmniSocials::NOT_GIVEN,
285
304
  linkedin_poll: nil, draft: nil, media_fit: nil,
286
- comment_dm_automation: OmniSocials::NOT_GIVEN)
305
+ comment_dm_automation: OmniSocials::NOT_GIVEN,
306
+ tags: OmniSocials::NOT_GIVEN)
287
307
  body = Internal.drop_nil(
288
308
  {
289
309
  "content" => content,
@@ -318,6 +338,8 @@ module OmniSocials
318
338
  body["comment_dm_automation"] = comment_dm_automation unless comment_dm_automation.is_a?(NotGiven)
319
339
  # Same for discord: an explicit nil removes every Discord option.
320
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)
321
343
  @client.request("PATCH", "/posts/#{post_id}", json: body)
322
344
  end
323
345
 
@@ -395,7 +417,7 @@ module OmniSocials
395
417
  threads:, google_business:, linkedin_poll:,
396
418
  telegram: nil, discord: nil, approval_workflow_id: nil,
397
419
  draft: nil,
398
- media_fit: nil, comment_dm_automation: nil)
420
+ media_fit: nil, comment_dm_automation: nil, tags: nil)
399
421
  Internal.drop_nil(
400
422
  {
401
423
  "content" => content,
@@ -434,7 +456,8 @@ module OmniSocials
434
456
  "approval_workflow_id" => approval_workflow_id,
435
457
  "draft" => draft,
436
458
  "media_fit" => media_fit,
437
- "comment_dm_automation" => comment_dm_automation
459
+ "comment_dm_automation" => comment_dm_automation,
460
+ "tags" => tags
438
461
  }
439
462
  )
440
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.13.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.13.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-27 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