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 +4 -4
- data/README.md +71 -4
- data/lib/omnisocials/client.rb +2 -1
- data/lib/omnisocials/resources/analytics.rb +34 -3
- data/lib/omnisocials/resources/automations.rb +68 -30
- data/lib/omnisocials/resources/inbox.rb +29 -3
- data/lib/omnisocials/resources/posts.rb +91 -31
- data/lib/omnisocials/resources/tags.rb +86 -0
- data/lib/omnisocials/version.rb +1 -1
- data/lib/omnisocials.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27468385685af2b1f49e6665393bb687f2dd9fc18cf7d06b98190c78c9b3c3fe
|
|
4
|
+
data.tar.gz: 24dafc551dc62fd9922aa9ce330ae138288a3ecd86dc768abcf35e2f5f466cae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
625
|
+
puts "X inbox is suspended until a top-up."
|
|
559
626
|
else
|
|
560
627
|
raise
|
|
561
628
|
end
|
data/lib/omnisocials/client.rb
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
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.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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")
|
|
19
|
-
#
|
|
20
|
-
# scope (opt-in on the API
|
|
21
|
-
|
|
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.
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
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".
|
|
53
|
-
# comment, within 7 days; OmniSocials
|
|
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
|
|
59
|
-
#
|
|
60
|
-
#
|
|
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
|
|
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
|
|
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"
|
|
101
|
-
#
|
|
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
|
|
108
|
-
#
|
|
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:
|
|
222
|
-
# to `reply` on comment threads, its "conversation_id" is what
|
|
223
|
-
# takes. "messages" is the conversation so far, oldest first (the most
|
|
247
|
+
# is what `hide`, `like` and `delete_message` take and the message_id:
|
|
248
|
+
# to pass to `reply` on comment threads, its "conversation_id" is what
|
|
249
|
+
# `reply` takes. "messages" is the conversation so far, oldest first (the most
|
|
224
250
|
# recent 50 messages for long DM threads). "reply_window" is
|
|
225
251
|
# { "open" => Boolean, "closes_at" => String|nil }: "open" is false
|
|
226
252
|
# only for an Instagram/Facebook DM past its 24-hour window, which
|
|
@@ -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
|
-
|
|
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: {
|
|
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
|
|
134
|
-
# with a DM, once per person, with an optional public
|
|
135
|
-
# comment. Shape: { "trigger" => { "keywords" =>
|
|
136
|
-
# "word" }, "actions" => { "dm" => { "text" =>
|
|
137
|
-
#
|
|
138
|
-
# { "max_sends" => 500 }
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
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"
|
|
144
|
-
#
|
|
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
|
|
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,
|
|
194
|
-
|
|
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
|
|
244
|
-
#
|
|
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,
|
|
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,
|
|
362
|
-
|
|
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
|
data/lib/omnisocials/version.rb
CHANGED
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.
|
|
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-
|
|
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
|