postproxy-sdk 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +263 -0
- data/lib/postproxy/client.rb +11 -1
- data/lib/postproxy/errors.rb +6 -0
- data/lib/postproxy/resources/chats.rb +17 -6
- data/lib/postproxy/resources/comments.rb +67 -15
- data/lib/postproxy/resources/messages.rb +40 -9
- data/lib/postproxy/resources/posts.rb +28 -12
- data/lib/postproxy/resources/profile_comments.rb +6 -4
- data/lib/postproxy/resources/profile_groups.rb +7 -4
- data/lib/postproxy/resources/profiles.rb +62 -8
- data/lib/postproxy/resources/queues.rb +8 -6
- data/lib/postproxy/resources/webhooks.rb +6 -6
- data/lib/postproxy/types.rb +207 -2
- data/lib/postproxy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6be6a31464b3afaf473473d94fd639b2553782cf0494e87ccfebb3e5e2e5e4a
|
|
4
|
+
data.tar.gz: 0ff052e8410c536e2938cf6887cecee713f615afa11207c88f733c29b6770d07
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a67b96326d47a4c33463c10b5346ff28debf6e4faf7ef11a297fded79abf9cac4b062099cc1e1cf6002e6e9f2bc4b9e641dbf26537552f8d2041310598a8163
|
|
7
|
+
data.tar.gz: cf8c37298d421c37886440e75819a9a1462d34ccadad79e4623cbb2cb431292e4ead2e1ba0014bd13b522c9c0e62737eeb524987bde5a97f99404e3416686950
|
data/README.md
CHANGED
|
@@ -50,6 +50,36 @@ end
|
|
|
50
50
|
client = PostProxy::Client.new("your-api-key", faraday_client: faraday)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Idempotency
|
|
54
|
+
|
|
55
|
+
Every write method (`POST`/`PUT`/`PATCH`/`DELETE`) accepts an `idempotency_key:`, sent as
|
|
56
|
+
the `Idempotency-Key` header. If the connection drops before you see the response, retry
|
|
57
|
+
with the same key and you get the original response back instead of a second post:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "securerandom"
|
|
61
|
+
|
|
62
|
+
key = SecureRandom.uuid
|
|
63
|
+
post = client.posts.create("Hello", profiles: ["profile-id"], idempotency_key: key)
|
|
64
|
+
|
|
65
|
+
# Retrying the same call with the same key replays the original response.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Generate a fresh key per logical operation — a UUID is ideal. Keys are scoped to your
|
|
69
|
+
account and may be up to 255 characters. The SDK never generates keys or retries for you.
|
|
70
|
+
|
|
71
|
+
| Situation | Result |
|
|
72
|
+
|---|---|
|
|
73
|
+
| First request with the key | Runs normally |
|
|
74
|
+
| Retry after a success | Original status and body replayed |
|
|
75
|
+
| Retry while the first is still running | `ConflictError` (409) — wait and retry |
|
|
76
|
+
| Same key, different request body | `ValidationError` (422) |
|
|
77
|
+
| Retry after an error response | Runs normally — errors are not replayed |
|
|
78
|
+
|
|
79
|
+
Only successful (`2xx`) responses are stored, so a request that failed validation or hit a
|
|
80
|
+
quota leaves the key free — fix the payload and retry with the same key. Stored responses
|
|
81
|
+
are kept for **24 hours**. Requests without a key are unaffected.
|
|
82
|
+
|
|
53
83
|
## Posts
|
|
54
84
|
|
|
55
85
|
```ruby
|
|
@@ -306,6 +336,15 @@ end
|
|
|
306
336
|
# List with pagination
|
|
307
337
|
comments = client.comments.list("post-id", profile_id: "profile-id", page: 2, per_page: 10)
|
|
308
338
|
|
|
339
|
+
# Filter by when PostProxy received the comment (created_at, not posted_at).
|
|
340
|
+
# A bare date means that date's start of day. Applies to top-level comments —
|
|
341
|
+
# one in range brings its full replies array with it.
|
|
342
|
+
recent = client.comments.list("post-id",
|
|
343
|
+
profile_id: "profile-id",
|
|
344
|
+
from: "2026-03-25",
|
|
345
|
+
to: "2026-03-26T12:00:00Z"
|
|
346
|
+
)
|
|
347
|
+
|
|
309
348
|
# Get a single comment
|
|
310
349
|
comment = client.comments.get("post-id", "comment-id", profile_id: "profile-id")
|
|
311
350
|
|
|
@@ -337,6 +376,38 @@ message = client.comments.private_reply("post-id", "comment-id", profile_id: "pr
|
|
|
337
376
|
puts message.chat_id, message.status
|
|
338
377
|
```
|
|
339
378
|
|
|
379
|
+
### Comments across posts
|
|
380
|
+
|
|
381
|
+
`comments.list_all` returns comments spanning every post in the profile group in one
|
|
382
|
+
request — the comments counterpart to `posts.stats`. Every filter is optional.
|
|
383
|
+
|
|
384
|
+
**This list is flat.** Unlike the per-post list, replies are not nested: every comment,
|
|
385
|
+
top-level or reply, is its own entry linked to its parent by `parent_external_id`, so
|
|
386
|
+
`total` counts every comment and paging is exact.
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
all = client.comments.list_all(
|
|
390
|
+
profiles: ["instagram", "prof-abc"], # profile IDs or network names, mixed
|
|
391
|
+
post_ids: ["post-1", "post-2"], # omit for every post in scope
|
|
392
|
+
from: "2026-03-25",
|
|
393
|
+
per_page: 50 # max 100
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
all.data.each do |c|
|
|
397
|
+
# Each entry says where it came from, so you can act on it with the
|
|
398
|
+
# post-scoped methods above.
|
|
399
|
+
puts "#{c.platform} #{c.post_id} #{c.profile_id}: #{c.body}"
|
|
400
|
+
puts " ↳ reply to #{c.parent_external_id}" if c.parent_external_id
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Reply to one of them
|
|
404
|
+
first = all.data.first
|
|
405
|
+
client.comments.create(first.post_id, "Thanks!", profile_id: first.profile_id, parent_id: first.id)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Unknown or out-of-scope IDs in `post_ids` and `profiles` are ignored rather than erroring.
|
|
409
|
+
Results are ordered newest first by receipt time.
|
|
410
|
+
|
|
340
411
|
## Direct Messages
|
|
341
412
|
|
|
342
413
|
Read and send 1:1 messages on DM-capable profiles (Facebook Messenger, Instagram, Telegram, Bluesky). A conversation is a **Chat**; it holds **Messages**. Outbound sends are processed asynchronously (`status` starts as `pending`).
|
|
@@ -393,6 +464,73 @@ message = client.comments.private_reply("post-id", "comment-id", profile_id: "pr
|
|
|
393
464
|
puts message.chat_id, message.status
|
|
394
465
|
```
|
|
395
466
|
|
|
467
|
+
### Quick replies and buttons (Facebook & Instagram)
|
|
468
|
+
|
|
469
|
+
Meta's two interactive primitives. **Quick replies** are chips above the participant's
|
|
470
|
+
composer that disappear once tapped; **buttons** are attached to the message and stay in
|
|
471
|
+
the thread. Telegram's equivalent is `reply_markup` above — passing `quick_replies` or
|
|
472
|
+
`buttons` on a Telegram or Bluesky chat returns `422`.
|
|
473
|
+
|
|
474
|
+
Each param accepts model instances or plain hashes, whichever you prefer:
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
# Quick replies — up to 13. title ≤ 20 chars, payload ≤ 1000.
|
|
478
|
+
client.messages.send(
|
|
479
|
+
chat.id,
|
|
480
|
+
body: "What can I help with?",
|
|
481
|
+
quick_replies: [
|
|
482
|
+
PostProxy::QuickReply.new(title: "Track order", payload: "TRACK"),
|
|
483
|
+
{ title: "Talk to support", payload: "HELP" }
|
|
484
|
+
]
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Buttons — up to 3, each either web_url or postback. card is optional and
|
|
488
|
+
# requires buttons.
|
|
489
|
+
client.messages.send(
|
|
490
|
+
chat.id,
|
|
491
|
+
body: "Your order shipped",
|
|
492
|
+
buttons: [
|
|
493
|
+
PostProxy::MessageButton.new(type: "web_url", title: "Track", url: "https://shop.example.com/o/123"),
|
|
494
|
+
PostProxy::MessageButton.new(type: "postback", title: "Cancel", payload: "CANCEL:123")
|
|
495
|
+
],
|
|
496
|
+
card: PostProxy::MessageCard.new(
|
|
497
|
+
subtitle: "Arriving Friday",
|
|
498
|
+
image_url: "https://cdn.example.com/shoe.png",
|
|
499
|
+
default_action: { type: "web_url", url: "https://shop.example.com/o/123" }
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Buttons are delivered as a Meta generic template and your `body` becomes the template's
|
|
505
|
+
element title — so **`body` is capped at 80 characters when buttons are present**. That is
|
|
506
|
+
Meta's limit, not PostProxy's, and a longer body is rejected with a `422` naming the
|
|
507
|
+
length. Buttons cannot be combined with media. Instagram is stricter than Messenger: it
|
|
508
|
+
delivers quick replies only on a plain-text message, so `quick_replies` with media or with
|
|
509
|
+
`buttons` returns `422` on Instagram while both are accepted on Facebook.
|
|
510
|
+
|
|
511
|
+
Validation happens server-side and names the offending index — `buttons[1].url must be an
|
|
512
|
+
https:// URL` — surfacing as the SDK's usual error for a `422`.
|
|
513
|
+
|
|
514
|
+
> The new params are sent on the JSON path only. To combine quick replies with an
|
|
515
|
+
> attachment, pass `media` as a hosted URL rather than uploading via `media_files`.
|
|
516
|
+
|
|
517
|
+
A tap comes back as an **inbound message** carrying `tapped_action`:
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
520
|
+
inbound = client.messages.list(chat.id, direction: "inbound")
|
|
521
|
+
inbound.data.each do |msg|
|
|
522
|
+
next unless msg.tapped_action
|
|
523
|
+
|
|
524
|
+
# kind: "quick_reply", "postback", or "callback_query"
|
|
525
|
+
puts "#{msg.tapped_action.kind}: #{msg.tapped_action.payload}"
|
|
526
|
+
end
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Subscribe to `message.received` to react to taps as they happen — the same field is on the
|
|
530
|
+
webhook payload. `tapped_action` is derived rather than stored, so it also resolves for
|
|
531
|
+
taps recorded before PostProxy exposed it, including Instagram ice-breaker taps and
|
|
532
|
+
Telegram callback queries (`kind` `"callback_query"`). A tap also opens the 24h window.
|
|
533
|
+
|
|
396
534
|
## Profile comments (Google Business reviews)
|
|
397
535
|
|
|
398
536
|
Profile-level comments expose Google Business reviews and replies. Reviews are user-generated — the SDK lets you list/get them and reply to or delete your own replies. Reviews sync twice daily.
|
|
@@ -466,6 +604,81 @@ bsky = client.profiles.get_profile_stats("prof_bsky_001")
|
|
|
466
604
|
puts bsky.data.records.last.stats[:followersCount]
|
|
467
605
|
```
|
|
468
606
|
|
|
607
|
+
Every stats record (post stats and profile stats alike) carries `raw_stats` alongside the
|
|
608
|
+
normalized `stats`, exposing each metric under its **original platform name**:
|
|
609
|
+
|
|
610
|
+
```ruby
|
|
611
|
+
stats = client.posts.stats(["post-id"])
|
|
612
|
+
record = stats.data["post-id"].platforms.first.records.first
|
|
613
|
+
|
|
614
|
+
puts record.stats[:impressions] # normalized
|
|
615
|
+
puts record.raw_stats[:views] # Instagram's own name
|
|
616
|
+
puts record.raw_stats[:impression_count] # Twitter/X's own name
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
LinkedIn post stats now normalize `likes`, `comments`, `shares`, and `clicks` alongside
|
|
620
|
+
`impressions` — previously only `impressions` was normalized.
|
|
621
|
+
|
|
622
|
+
### Post syncs & backfill
|
|
623
|
+
|
|
624
|
+
PostProxy mirrors posts published natively on a platform into your account. Every one of
|
|
625
|
+
those pulls is recorded as a **post sync**: the one fired when the profile connects, the
|
|
626
|
+
recurring poll, and any backfill you start.
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
# Start a backfill — walks the feed backwards from the newest post in batches
|
|
630
|
+
# of 25 until it reaches `from` or the platform stops returning posts.
|
|
631
|
+
sync = client.profiles.backfill_posts("prof-id", from: "2025-01-01")
|
|
632
|
+
puts sync.id, sync.status # => "sync456def" "pending"
|
|
633
|
+
|
|
634
|
+
# Poll it to completion — finished when status is "completed" or "failed"
|
|
635
|
+
run = client.profiles.post_sync("prof-id", sync.id)
|
|
636
|
+
puts "#{run.posts_imported} of #{run.posts_seen}, back to #{run.oldest_posted_at}"
|
|
637
|
+
|
|
638
|
+
# List recent runs (kept for 30 days), newest first
|
|
639
|
+
runs = client.profiles.post_syncs("prof-id",
|
|
640
|
+
trigger: "backfill", # connect | scheduled | backfill
|
|
641
|
+
status: "completed", # pending | running | completed | failed
|
|
642
|
+
per_page: 25
|
|
643
|
+
)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
| `PostSync` field | Description |
|
|
647
|
+
|---|---|
|
|
648
|
+
| `id` | Sync identifier |
|
|
649
|
+
| `profile_id` | Profile this run belongs to |
|
|
650
|
+
| `kind` | Always `posts` today |
|
|
651
|
+
| `trigger` | `connect`, `scheduled`, or `backfill` |
|
|
652
|
+
| `status` | `pending`, `running`, `completed`, or `failed` |
|
|
653
|
+
| `started_at` / `completed_at` | `Time` or `nil` |
|
|
654
|
+
| `posts_seen` | Posts the platform returned across the run |
|
|
655
|
+
| `posts_imported` | Posts that were **new** and got created |
|
|
656
|
+
| `backfill_from` | The date floor requested; `nil` for `connect`/`scheduled` |
|
|
657
|
+
| `oldest_posted_at` | Publish date of the oldest post the run reached |
|
|
658
|
+
| `error` | Platform error message when `status` is `"failed"` |
|
|
659
|
+
| `created_at` | `Time` |
|
|
660
|
+
|
|
661
|
+
**How far back a backfill reaches depends on the platform's API**, not on PostProxy: where
|
|
662
|
+
history is pageable we follow it, otherwise the run ends early with whatever it got and
|
|
663
|
+
still reports `status == "completed"`.
|
|
664
|
+
|
|
665
|
+
Only one backfill runs per profile at a time — starting a second raises `ConflictError`
|
|
666
|
+
carrying the running one's id:
|
|
667
|
+
|
|
668
|
+
```ruby
|
|
669
|
+
begin
|
|
670
|
+
client.profiles.backfill_posts("prof-id", from: "2025-01-01")
|
|
671
|
+
rescue PostProxy::ConflictError => e
|
|
672
|
+
running_id = e.response[:profile_sync_id]
|
|
673
|
+
# Poll the run that's already going.
|
|
674
|
+
end
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Posts you already have are skipped, so overlapping backfills are safe. Imported posts
|
|
678
|
+
behave exactly like ones the poll picks up (`source: "imported"`, `post.imported`
|
|
679
|
+
webhook), but a backfill's follow-up work is queued at a lower priority so a deep run
|
|
680
|
+
can't slow down publishing.
|
|
681
|
+
|
|
469
682
|
## Profile Groups
|
|
470
683
|
|
|
471
684
|
```ruby
|
|
@@ -560,6 +773,43 @@ post = client.posts.create(
|
|
|
560
773
|
|
|
561
774
|
Supported platforms: `facebook`, `instagram`, `tiktok`, `linkedin`, `youtube`, `twitter`, `threads`, `pinterest`, `bluesky`, `telegram`, `google_business`. Telegram requires a `chat_id` per post — list channels with `client.profiles.placements(profile_id)`.
|
|
562
775
|
|
|
776
|
+
### Instagram user tags
|
|
777
|
+
|
|
778
|
+
Tag public Instagram accounts in a post — feed post, reel, or story:
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
client.posts.create(
|
|
782
|
+
"Shot on location",
|
|
783
|
+
profiles: ["ig-profile-id"],
|
|
784
|
+
media: [
|
|
785
|
+
"https://example.com/1.jpg",
|
|
786
|
+
"https://example.com/2.jpg",
|
|
787
|
+
"https://example.com/3.mp4"
|
|
788
|
+
],
|
|
789
|
+
platforms: PostProxy::PlatformParams.new(
|
|
790
|
+
instagram: PostProxy::InstagramParams.new(
|
|
791
|
+
format: "post",
|
|
792
|
+
user_tags: [
|
|
793
|
+
{ username: "natgeo", x: 0.5, y: 0.4 }, # slide 0
|
|
794
|
+
{ username: "nasa", x: 0.2, y: 0.8, media_index: 1 }, # slide 1
|
|
795
|
+
{ username: "spacex", media_index: 2 } # video — username only
|
|
796
|
+
]
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
- **Images require `x` and `y`** — floats `0.0`–`1.0` measured from the top-left corner.
|
|
803
|
+
- **Reels and video slides** are tagged by username only; coordinates are ignored and dropped.
|
|
804
|
+
- **Stories** accept coordinates but don't need them.
|
|
805
|
+
- `media_index` picks the carousel slide (0-based, defaults to `0`, video slides included).
|
|
806
|
+
- A leading `@` on a username is stripped for you.
|
|
807
|
+
|
|
808
|
+
Coordinates outside `0.0`–`1.0`, a `media_index` past the last media item, or an image tag
|
|
809
|
+
missing `x`/`y` are rejected with a `ValidationError` naming the offending entry. Accounts
|
|
810
|
+
that are private or have tagging turned off are silently skipped by Instagram at publish
|
|
811
|
+
time.
|
|
812
|
+
|
|
563
813
|
### Google Business
|
|
564
814
|
|
|
565
815
|
Google Business posts use a `google_business` entry in `PlatformParams` (a plain hash; no typed struct). The `location_id` is the location resource path returned by `client.profiles.placements()`. Supported formats: `standard`, `event`, `offer`. CTA actions: `LEARN_MORE`, `BOOK`, `ORDER`, `SHOP`, `SIGN_UP`, `CALL`. Media is limited to one image (≤5 MB).
|
|
@@ -589,6 +839,10 @@ rescue PostProxy::AuthenticationError => e
|
|
|
589
839
|
puts "Auth failed: #{e.message}" # 401
|
|
590
840
|
rescue PostProxy::NotFoundError => e
|
|
591
841
|
puts "Not found: #{e.message}" # 404
|
|
842
|
+
rescue PostProxy::ConflictError => e
|
|
843
|
+
puts "Conflict: #{e.message}" # 409
|
|
844
|
+
puts e.response[:duplicate_post_id] # on a duplicate post
|
|
845
|
+
puts e.response[:profile_sync_id] # on a backfill already running
|
|
592
846
|
rescue PostProxy::ValidationError => e
|
|
593
847
|
puts "Invalid: #{e.message}" # 422
|
|
594
848
|
rescue PostProxy::BadRequestError => e
|
|
@@ -599,6 +853,15 @@ rescue PostProxy::Error => e
|
|
|
599
853
|
end
|
|
600
854
|
```
|
|
601
855
|
|
|
856
|
+
| Status | Error | Raised for |
|
|
857
|
+
|---|---|---|
|
|
858
|
+
| 400 | `BadRequestError` | Missing required parameters |
|
|
859
|
+
| 401 | `AuthenticationError` | Invalid, missing, or insufficient API key permissions |
|
|
860
|
+
| 404 | `NotFoundError` | Resource does not exist or is not accessible |
|
|
861
|
+
| 409 | `ConflictError` | Duplicate submission, a backfill already running, or an in-flight `Idempotency-Key` |
|
|
862
|
+
| 422 | `ValidationError` | Validation failed |
|
|
863
|
+
| 429 | `Error` | Posting rate limit reached |
|
|
864
|
+
|
|
602
865
|
## License
|
|
603
866
|
|
|
604
867
|
MIT
|
data/lib/postproxy/client.rb
CHANGED
|
@@ -67,7 +67,8 @@ module PostProxy
|
|
|
67
67
|
@messages ||= Resources::Messages.new(self)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil
|
|
70
|
+
def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil,
|
|
71
|
+
idempotency_key: nil)
|
|
71
72
|
url = "/api#{path}"
|
|
72
73
|
|
|
73
74
|
query = {}
|
|
@@ -102,12 +103,14 @@ module PostProxy
|
|
|
102
103
|
end
|
|
103
104
|
conn.send(method, url) do |req|
|
|
104
105
|
req.params = query unless query.empty?
|
|
106
|
+
req.headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
105
107
|
req.body = payload
|
|
106
108
|
end
|
|
107
109
|
else
|
|
108
110
|
conn = json_connection
|
|
109
111
|
conn.send(method, url) do |req|
|
|
110
112
|
req.params = query unless query.empty?
|
|
113
|
+
req.headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
111
114
|
req.body = json.to_json if json
|
|
112
115
|
end
|
|
113
116
|
end
|
|
@@ -160,6 +163,13 @@ module PostProxy
|
|
|
160
163
|
status_code: response.status,
|
|
161
164
|
response: body
|
|
162
165
|
)
|
|
166
|
+
when 409
|
|
167
|
+
body = parse_error_body(response)
|
|
168
|
+
raise ConflictError.new(
|
|
169
|
+
error_message(body),
|
|
170
|
+
status_code: response.status,
|
|
171
|
+
response: body
|
|
172
|
+
)
|
|
163
173
|
when 422
|
|
164
174
|
body = parse_error_body(response)
|
|
165
175
|
raise ValidationError.new(
|
data/lib/postproxy/errors.rb
CHANGED
|
@@ -11,6 +11,12 @@ module PostProxy
|
|
|
11
11
|
|
|
12
12
|
class AuthenticationError < Error; end
|
|
13
13
|
class NotFoundError < Error; end
|
|
14
|
+
|
|
15
|
+
# 409. Raised for a duplicate submission (`response[:duplicate_post_id]`), a
|
|
16
|
+
# backfill that is already running (`response[:profile_sync_id]`), or a
|
|
17
|
+
# request whose Idempotency-Key is still in flight.
|
|
18
|
+
class ConflictError < Error; end
|
|
19
|
+
|
|
14
20
|
class ValidationError < Error; end
|
|
15
21
|
class BadRequestError < Error; end
|
|
16
22
|
end
|
|
@@ -22,12 +22,17 @@ module PostProxy
|
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def create(profile_id, participant_external_id, participant_username: nil, participant_name: nil,
|
|
25
|
+
def create(profile_id, participant_external_id, participant_username: nil, participant_name: nil,
|
|
26
|
+
profile_group_id: nil, idempotency_key: nil)
|
|
26
27
|
json_body = { participant_external_id: participant_external_id }
|
|
27
28
|
json_body[:participant_username] = participant_username if participant_username
|
|
28
29
|
json_body[:participant_name] = participant_name if participant_name
|
|
29
30
|
|
|
30
|
-
result = @client.request(:post, "/profiles/#{profile_id}/chats",
|
|
31
|
+
result = @client.request(:post, "/profiles/#{profile_id}/chats",
|
|
32
|
+
json: json_body,
|
|
33
|
+
profile_group_id: profile_group_id,
|
|
34
|
+
idempotency_key: idempotency_key
|
|
35
|
+
)
|
|
31
36
|
Chat.new(**result)
|
|
32
37
|
end
|
|
33
38
|
|
|
@@ -36,13 +41,19 @@ module PostProxy
|
|
|
36
41
|
Chat.new(**result)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
|
-
def archive(chat_id, profile_group_id: nil)
|
|
40
|
-
result = @client.request(:post, "/chats/#{chat_id}/archive",
|
|
44
|
+
def archive(chat_id, profile_group_id: nil, idempotency_key: nil)
|
|
45
|
+
result = @client.request(:post, "/chats/#{chat_id}/archive",
|
|
46
|
+
profile_group_id: profile_group_id,
|
|
47
|
+
idempotency_key: idempotency_key
|
|
48
|
+
)
|
|
41
49
|
Chat.new(**result)
|
|
42
50
|
end
|
|
43
51
|
|
|
44
|
-
def unarchive(chat_id, profile_group_id: nil)
|
|
45
|
-
result = @client.request(:delete, "/chats/#{chat_id}/archive",
|
|
52
|
+
def unarchive(chat_id, profile_group_id: nil, idempotency_key: nil)
|
|
53
|
+
result = @client.request(:delete, "/chats/#{chat_id}/archive",
|
|
54
|
+
profile_group_id: profile_group_id,
|
|
55
|
+
idempotency_key: idempotency_key
|
|
56
|
+
)
|
|
46
57
|
Chat.new(**result)
|
|
47
58
|
end
|
|
48
59
|
|
|
@@ -5,10 +5,15 @@ module PostProxy
|
|
|
5
5
|
@client = client
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# `from` and `to` filter on when PostProxy received the comment
|
|
9
|
+
# (`created_at`), not the platform's `posted_at`. They apply to top-level
|
|
10
|
+
# comments — one in range brings its full `replies` array with it.
|
|
11
|
+
def list(post_id, profile_id:, page: nil, per_page: nil, from: nil, to: nil)
|
|
9
12
|
params = { profile_id: profile_id }
|
|
10
13
|
params[:page] = page if page
|
|
11
14
|
params[:per_page] = per_page if per_page
|
|
15
|
+
params[:from] = from if from
|
|
16
|
+
params[:to] = to if to
|
|
12
17
|
|
|
13
18
|
result = @client.request(:get, "/posts/#{post_id}/comments", params: params)
|
|
14
19
|
comments = (result[:data] || []).map { |c| Comment.new(**c) }
|
|
@@ -20,48 +25,95 @@ module PostProxy
|
|
|
20
25
|
)
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# Comments across every post in the profile group. Flat: replies come
|
|
29
|
+
# back as their own entries linked by `parent_external_id`, so `total`
|
|
30
|
+
# counts every comment. `profiles` takes profile IDs or network names,
|
|
31
|
+
# mixed.
|
|
32
|
+
def list_all(post_ids: nil, profiles: nil, from: nil, to: nil, page: nil, per_page: nil,
|
|
33
|
+
profile_group_id: nil)
|
|
34
|
+
params = {}
|
|
35
|
+
params[:post_ids] = Array(post_ids).join(",") if post_ids
|
|
36
|
+
params[:profiles] = Array(profiles).join(",") if profiles
|
|
37
|
+
params[:from] = from if from
|
|
38
|
+
params[:to] = to if to
|
|
39
|
+
params[:page] = page if page
|
|
40
|
+
params[:per_page] = per_page if per_page
|
|
41
|
+
|
|
42
|
+
result = @client.request(:get, "/comments",
|
|
43
|
+
params: params.empty? ? nil : params,
|
|
44
|
+
profile_group_id: profile_group_id
|
|
45
|
+
)
|
|
46
|
+
comments = (result[:data] || []).map { |c| BulkComment.new(**c) }
|
|
47
|
+
PaginatedResponse.new(
|
|
48
|
+
data: comments,
|
|
49
|
+
total: result[:total],
|
|
50
|
+
page: result[:page],
|
|
51
|
+
per_page: result[:per_page]
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
23
55
|
def get(post_id, comment_id, profile_id:)
|
|
24
56
|
result = @client.request(:get, "/posts/#{post_id}/comments/#{comment_id}", params: { profile_id: profile_id })
|
|
25
57
|
Comment.new(**result)
|
|
26
58
|
end
|
|
27
59
|
|
|
28
|
-
def create(post_id, text, profile_id:, parent_id: nil)
|
|
60
|
+
def create(post_id, text, profile_id:, parent_id: nil, idempotency_key: nil)
|
|
29
61
|
json_body = { text: text }
|
|
30
62
|
json_body[:parent_id] = parent_id if parent_id
|
|
31
63
|
|
|
32
|
-
result = @client.request(:post, "/posts/#{post_id}/comments",
|
|
64
|
+
result = @client.request(:post, "/posts/#{post_id}/comments",
|
|
65
|
+
params: { profile_id: profile_id },
|
|
66
|
+
json: json_body,
|
|
67
|
+
idempotency_key: idempotency_key
|
|
68
|
+
)
|
|
33
69
|
Comment.new(**result)
|
|
34
70
|
end
|
|
35
71
|
|
|
36
|
-
def delete(post_id, comment_id, profile_id:)
|
|
37
|
-
result = @client.request(:delete, "/posts/#{post_id}/comments/#{comment_id}",
|
|
72
|
+
def delete(post_id, comment_id, profile_id:, idempotency_key: nil)
|
|
73
|
+
result = @client.request(:delete, "/posts/#{post_id}/comments/#{comment_id}",
|
|
74
|
+
params: { profile_id: profile_id },
|
|
75
|
+
idempotency_key: idempotency_key
|
|
76
|
+
)
|
|
38
77
|
AcceptedResponse.new(**result)
|
|
39
78
|
end
|
|
40
79
|
|
|
41
|
-
def hide(post_id, comment_id, profile_id:)
|
|
42
|
-
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/hide",
|
|
80
|
+
def hide(post_id, comment_id, profile_id:, idempotency_key: nil)
|
|
81
|
+
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/hide",
|
|
82
|
+
params: { profile_id: profile_id },
|
|
83
|
+
idempotency_key: idempotency_key
|
|
84
|
+
)
|
|
43
85
|
AcceptedResponse.new(**result)
|
|
44
86
|
end
|
|
45
87
|
|
|
46
|
-
def unhide(post_id, comment_id, profile_id:)
|
|
47
|
-
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/unhide",
|
|
88
|
+
def unhide(post_id, comment_id, profile_id:, idempotency_key: nil)
|
|
89
|
+
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/unhide",
|
|
90
|
+
params: { profile_id: profile_id },
|
|
91
|
+
idempotency_key: idempotency_key
|
|
92
|
+
)
|
|
48
93
|
AcceptedResponse.new(**result)
|
|
49
94
|
end
|
|
50
95
|
|
|
51
|
-
def like(post_id, comment_id, profile_id:)
|
|
52
|
-
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/like",
|
|
96
|
+
def like(post_id, comment_id, profile_id:, idempotency_key: nil)
|
|
97
|
+
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/like",
|
|
98
|
+
params: { profile_id: profile_id },
|
|
99
|
+
idempotency_key: idempotency_key
|
|
100
|
+
)
|
|
53
101
|
AcceptedResponse.new(**result)
|
|
54
102
|
end
|
|
55
103
|
|
|
56
|
-
def unlike(post_id, comment_id, profile_id:)
|
|
57
|
-
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/unlike",
|
|
104
|
+
def unlike(post_id, comment_id, profile_id:, idempotency_key: nil)
|
|
105
|
+
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/unlike",
|
|
106
|
+
params: { profile_id: profile_id },
|
|
107
|
+
idempotency_key: idempotency_key
|
|
108
|
+
)
|
|
58
109
|
AcceptedResponse.new(**result)
|
|
59
110
|
end
|
|
60
111
|
|
|
61
|
-
def private_reply(post_id, comment_id, profile_id:, text:)
|
|
112
|
+
def private_reply(post_id, comment_id, profile_id:, text:, idempotency_key: nil)
|
|
62
113
|
result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/private_reply",
|
|
63
114
|
params: { profile_id: profile_id },
|
|
64
|
-
json: { text: text }
|
|
115
|
+
json: { text: text },
|
|
116
|
+
idempotency_key: idempotency_key
|
|
65
117
|
)
|
|
66
118
|
Message.new(**result)
|
|
67
119
|
end
|
|
@@ -22,8 +22,16 @@ module PostProxy
|
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# quick_replies, buttons, and card are Facebook and Instagram only — they
|
|
26
|
+
# return 422 on Telegram and Bluesky, where reply_markup is the
|
|
27
|
+
# equivalent. They are sent on the JSON path only, so pass media as hosted
|
|
28
|
+
# URLs rather than media_files when combining with an attachment.
|
|
29
|
+
#
|
|
30
|
+
# Each accepts model instances or plain hashes.
|
|
25
31
|
def send_message(chat_id, body: nil, media: nil, media_files: nil, tag: nil,
|
|
26
|
-
reply_to_external_id: nil, reply_markup: nil,
|
|
32
|
+
reply_to_external_id: nil, reply_markup: nil,
|
|
33
|
+
quick_replies: nil, buttons: nil, card: nil,
|
|
34
|
+
profile_group_id: nil, idempotency_key: nil)
|
|
27
35
|
has_files = media_files && !media_files.empty?
|
|
28
36
|
|
|
29
37
|
if has_files
|
|
@@ -47,7 +55,8 @@ module PostProxy
|
|
|
47
55
|
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
48
56
|
data: form_data,
|
|
49
57
|
files: files,
|
|
50
|
-
profile_group_id: profile_group_id
|
|
58
|
+
profile_group_id: profile_group_id,
|
|
59
|
+
idempotency_key: idempotency_key
|
|
51
60
|
)
|
|
52
61
|
else
|
|
53
62
|
json_body = {}
|
|
@@ -56,8 +65,15 @@ module PostProxy
|
|
|
56
65
|
json_body[:tag] = tag if tag
|
|
57
66
|
json_body[:reply_to_external_id] = reply_to_external_id if reply_to_external_id
|
|
58
67
|
json_body[:reply_markup] = reply_markup if reply_markup
|
|
68
|
+
json_body[:quick_replies] = quick_replies.map { |q| serialize_interactive(q) } if quick_replies
|
|
69
|
+
json_body[:buttons] = buttons.map { |b| serialize_interactive(b) } if buttons
|
|
70
|
+
json_body[:card] = serialize_interactive(card) if card
|
|
59
71
|
|
|
60
|
-
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
72
|
+
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
73
|
+
json: json_body,
|
|
74
|
+
profile_group_id: profile_group_id,
|
|
75
|
+
idempotency_key: idempotency_key
|
|
76
|
+
)
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
Message.new(**result)
|
|
@@ -69,34 +85,49 @@ module PostProxy
|
|
|
69
85
|
Message.new(**result)
|
|
70
86
|
end
|
|
71
87
|
|
|
72
|
-
def edit(message_id, body: nil, reply_markup: nil, profile_group_id: nil)
|
|
88
|
+
def edit(message_id, body: nil, reply_markup: nil, profile_group_id: nil, idempotency_key: nil)
|
|
73
89
|
json_body = {}
|
|
74
90
|
json_body[:body] = body if body
|
|
75
91
|
json_body[:reply_markup] = reply_markup if reply_markup
|
|
76
92
|
|
|
77
|
-
result = @client.request(:patch, "/messages/#{message_id}",
|
|
93
|
+
result = @client.request(:patch, "/messages/#{message_id}",
|
|
94
|
+
json: json_body,
|
|
95
|
+
profile_group_id: profile_group_id,
|
|
96
|
+
idempotency_key: idempotency_key
|
|
97
|
+
)
|
|
78
98
|
Message.new(**result)
|
|
79
99
|
end
|
|
80
100
|
|
|
81
|
-
def react(message_id, reaction: nil, emoji: nil, profile_group_id: nil)
|
|
101
|
+
def react(message_id, reaction: nil, emoji: nil, profile_group_id: nil, idempotency_key: nil)
|
|
82
102
|
json_body = {}
|
|
83
103
|
json_body[:reaction] = reaction if reaction
|
|
84
104
|
json_body[:emoji] = emoji if emoji
|
|
85
105
|
|
|
86
106
|
result = @client.request(:post, "/messages/#{message_id}/react",
|
|
87
107
|
json: json_body.empty? ? nil : json_body,
|
|
88
|
-
profile_group_id: profile_group_id
|
|
108
|
+
profile_group_id: profile_group_id,
|
|
109
|
+
idempotency_key: idempotency_key
|
|
89
110
|
)
|
|
90
111
|
Message.new(**result)
|
|
91
112
|
end
|
|
92
113
|
|
|
93
|
-
def unreact(message_id, profile_group_id: nil)
|
|
94
|
-
result = @client.request(:delete, "/messages/#{message_id}/unreact",
|
|
114
|
+
def unreact(message_id, profile_group_id: nil, idempotency_key: nil)
|
|
115
|
+
result = @client.request(:delete, "/messages/#{message_id}/unreact",
|
|
116
|
+
profile_group_id: profile_group_id,
|
|
117
|
+
idempotency_key: idempotency_key
|
|
118
|
+
)
|
|
95
119
|
Message.new(**result)
|
|
96
120
|
end
|
|
97
121
|
|
|
98
122
|
private
|
|
99
123
|
|
|
124
|
+
# Interactive params accept model instances or plain hashes. Models expose
|
|
125
|
+
# to_h with nils dropped, so an omitted content_type stays omitted rather
|
|
126
|
+
# than being sent as null.
|
|
127
|
+
def serialize_interactive(value)
|
|
128
|
+
value.respond_to?(:to_h) && !value.is_a?(Hash) ? value.to_h : value
|
|
129
|
+
end
|
|
130
|
+
|
|
100
131
|
def mime_type_for(filename)
|
|
101
132
|
case File.extname(filename).downcase
|
|
102
133
|
when ".jpg", ".jpeg" then "image/jpeg"
|
|
@@ -30,7 +30,7 @@ module PostProxy
|
|
|
30
30
|
|
|
31
31
|
def create(body, profiles:, media: nil, media_files: nil, platforms: nil,
|
|
32
32
|
thread: nil, scheduled_at: nil, draft: nil, queue_id: nil,
|
|
33
|
-
queue_priority: nil, profile_group_id: nil)
|
|
33
|
+
queue_priority: nil, profile_group_id: nil, idempotency_key: nil)
|
|
34
34
|
has_files = media_files && !media_files.empty?
|
|
35
35
|
has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
|
|
36
36
|
|
|
@@ -87,7 +87,8 @@ module PostProxy
|
|
|
87
87
|
result = @client.request(:post, "/posts",
|
|
88
88
|
data: form_data,
|
|
89
89
|
files: files,
|
|
90
|
-
profile_group_id: profile_group_id
|
|
90
|
+
profile_group_id: profile_group_id,
|
|
91
|
+
idempotency_key: idempotency_key
|
|
91
92
|
)
|
|
92
93
|
else
|
|
93
94
|
post_payload = { body: body }
|
|
@@ -101,7 +102,11 @@ module PostProxy
|
|
|
101
102
|
json_body[:queue_id] = queue_id if queue_id
|
|
102
103
|
json_body[:queue_priority] = queue_priority if queue_priority
|
|
103
104
|
|
|
104
|
-
result = @client.request(:post, "/posts",
|
|
105
|
+
result = @client.request(:post, "/posts",
|
|
106
|
+
json: json_body,
|
|
107
|
+
profile_group_id: profile_group_id,
|
|
108
|
+
idempotency_key: idempotency_key
|
|
109
|
+
)
|
|
105
110
|
end
|
|
106
111
|
|
|
107
112
|
Post.new(**result)
|
|
@@ -109,7 +114,7 @@ module PostProxy
|
|
|
109
114
|
|
|
110
115
|
def update(id, body: nil, profiles: nil, media: nil, media_files: nil, platforms: nil,
|
|
111
116
|
thread: nil, scheduled_at: nil, draft: nil, queue_id: nil,
|
|
112
|
-
queue_priority: nil, profile_group_id: nil)
|
|
117
|
+
queue_priority: nil, profile_group_id: nil, idempotency_key: nil)
|
|
113
118
|
has_files = media_files && !media_files.empty?
|
|
114
119
|
has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
|
|
115
120
|
|
|
@@ -167,7 +172,8 @@ module PostProxy
|
|
|
167
172
|
result = @client.request(:patch, "/posts/#{id}",
|
|
168
173
|
data: form_data,
|
|
169
174
|
files: files,
|
|
170
|
-
profile_group_id: profile_group_id
|
|
175
|
+
profile_group_id: profile_group_id,
|
|
176
|
+
idempotency_key: idempotency_key
|
|
171
177
|
)
|
|
172
178
|
else
|
|
173
179
|
json_body = {}
|
|
@@ -185,14 +191,21 @@ module PostProxy
|
|
|
185
191
|
json_body[:queue_id] = queue_id if queue_id
|
|
186
192
|
json_body[:queue_priority] = queue_priority if queue_priority
|
|
187
193
|
|
|
188
|
-
result = @client.request(:patch, "/posts/#{id}",
|
|
194
|
+
result = @client.request(:patch, "/posts/#{id}",
|
|
195
|
+
json: json_body,
|
|
196
|
+
profile_group_id: profile_group_id,
|
|
197
|
+
idempotency_key: idempotency_key
|
|
198
|
+
)
|
|
189
199
|
end
|
|
190
200
|
|
|
191
201
|
Post.new(**result)
|
|
192
202
|
end
|
|
193
203
|
|
|
194
|
-
def publish_draft(id, profile_group_id: nil)
|
|
195
|
-
result = @client.request(:post, "/posts/#{id}/publish",
|
|
204
|
+
def publish_draft(id, profile_group_id: nil, idempotency_key: nil)
|
|
205
|
+
result = @client.request(:post, "/posts/#{id}/publish",
|
|
206
|
+
profile_group_id: profile_group_id,
|
|
207
|
+
idempotency_key: idempotency_key
|
|
208
|
+
)
|
|
196
209
|
Post.new(**result)
|
|
197
210
|
end
|
|
198
211
|
|
|
@@ -209,24 +222,27 @@ module PostProxy
|
|
|
209
222
|
StatsResponse.new(data: posts)
|
|
210
223
|
end
|
|
211
224
|
|
|
212
|
-
def delete(id, delete_on_platform: nil, profile_group_id: nil)
|
|
225
|
+
def delete(id, delete_on_platform: nil, profile_group_id: nil, idempotency_key: nil)
|
|
213
226
|
params = {}
|
|
214
227
|
params[:delete_on_platform] = delete_on_platform unless delete_on_platform.nil?
|
|
215
228
|
result = @client.request(:delete, "/posts/#{id}",
|
|
216
229
|
params: params.empty? ? nil : params,
|
|
217
|
-
profile_group_id: profile_group_id
|
|
230
|
+
profile_group_id: profile_group_id,
|
|
231
|
+
idempotency_key: idempotency_key
|
|
218
232
|
)
|
|
219
233
|
DeleteResponse.new(**result)
|
|
220
234
|
end
|
|
221
235
|
|
|
222
|
-
def delete_on_platform(id, post_profile_id: nil, profile_id: nil, network: nil, profile_group_id: nil
|
|
236
|
+
def delete_on_platform(id, post_profile_id: nil, profile_id: nil, network: nil, profile_group_id: nil,
|
|
237
|
+
idempotency_key: nil)
|
|
223
238
|
json_body = {}
|
|
224
239
|
json_body[:post_profile_id] = post_profile_id if post_profile_id
|
|
225
240
|
json_body[:profile_id] = profile_id if profile_id
|
|
226
241
|
json_body[:network] = network if network
|
|
227
242
|
result = @client.request(:post, "/posts/#{id}/delete_on_platform",
|
|
228
243
|
json: json_body.empty? ? nil : json_body,
|
|
229
|
-
profile_group_id: profile_group_id
|
|
244
|
+
profile_group_id: profile_group_id,
|
|
245
|
+
idempotency_key: idempotency_key
|
|
230
246
|
)
|
|
231
247
|
DeleteOnPlatformResponse.new(**result)
|
|
232
248
|
end
|
|
@@ -26,14 +26,16 @@ module PostProxy
|
|
|
26
26
|
ProfileComment.new(**result)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def create(profile_id, parent_id:, text:)
|
|
29
|
+
def create(profile_id, parent_id:, text:, idempotency_key: nil)
|
|
30
30
|
result = @client.request(:post, "/profiles/#{profile_id}/comments",
|
|
31
|
-
json: { parent_id: parent_id, text: text }
|
|
31
|
+
json: { parent_id: parent_id, text: text },
|
|
32
|
+
idempotency_key: idempotency_key)
|
|
32
33
|
ProfileComment.new(**result)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def delete(profile_id, comment_id)
|
|
36
|
-
result = @client.request(:delete, "/profiles/#{profile_id}/comments/#{comment_id}"
|
|
36
|
+
def delete(profile_id, comment_id, idempotency_key: nil)
|
|
37
|
+
result = @client.request(:delete, "/profiles/#{profile_id}/comments/#{comment_id}",
|
|
38
|
+
idempotency_key: idempotency_key)
|
|
37
39
|
AcceptedResponse.new(**result)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
@@ -16,13 +16,16 @@ module PostProxy
|
|
|
16
16
|
ProfileGroup.new(**result)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def create(name)
|
|
20
|
-
result = @client.request(:post, "/profile_groups",
|
|
19
|
+
def create(name, idempotency_key: nil)
|
|
20
|
+
result = @client.request(:post, "/profile_groups",
|
|
21
|
+
json: { name: name },
|
|
22
|
+
idempotency_key: idempotency_key
|
|
23
|
+
)
|
|
21
24
|
ProfileGroup.new(**result)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
def delete(id)
|
|
25
|
-
result = @client.request(:delete, "/profile_groups/#{id}")
|
|
27
|
+
def delete(id, idempotency_key: nil)
|
|
28
|
+
result = @client.request(:delete, "/profile_groups/#{id}", idempotency_key: idempotency_key)
|
|
26
29
|
DeleteResponse.new(**result)
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -39,17 +39,64 @@ module PostProxy
|
|
|
39
39
|
# Moves a placement (e.g. a Facebook Page or Telegram channel) to another
|
|
40
40
|
# profile group. `placement_id` is the placement's external ID as
|
|
41
41
|
# returned by #placements.
|
|
42
|
-
def assign_placement_to_group(id, placement_id:, target_profile_group_id:, profile_group_id: nil
|
|
42
|
+
def assign_placement_to_group(id, placement_id:, target_profile_group_id:, profile_group_id: nil,
|
|
43
|
+
idempotency_key: nil)
|
|
43
44
|
result = @client.request(:patch, "/profiles/#{id}/assign_placement_to_group",
|
|
44
45
|
json: {
|
|
45
46
|
placement_id: placement_id,
|
|
46
47
|
target_profile_group_id: target_profile_group_id
|
|
47
48
|
},
|
|
48
|
-
profile_group_id: profile_group_id
|
|
49
|
+
profile_group_id: profile_group_id,
|
|
50
|
+
idempotency_key: idempotency_key
|
|
49
51
|
)
|
|
50
52
|
Placement.new(**result)
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
# Imports older posts from the platform. Walks the profile's feed
|
|
56
|
+
# backwards from the newest post until it reaches `from` or the platform
|
|
57
|
+
# stops returning posts. Runs in the background — poll #post_sync with
|
|
58
|
+
# the returned id for progress. Only one backfill runs per profile;
|
|
59
|
+
# starting a second raises ConflictError carrying the running one's
|
|
60
|
+
# `profile_sync_id`.
|
|
61
|
+
def backfill_posts(id, from:, profile_group_id: nil, idempotency_key: nil)
|
|
62
|
+
result = @client.request(:post, "/profiles/#{id}/backfill_posts",
|
|
63
|
+
json: { from: from },
|
|
64
|
+
profile_group_id: profile_group_id,
|
|
65
|
+
idempotency_key: idempotency_key
|
|
66
|
+
)
|
|
67
|
+
PostSync.new(**result)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Lists post sync runs, newest first. Runs are kept for 30 days.
|
|
71
|
+
def post_syncs(id, trigger: nil, status: nil, page: nil, per_page: nil, profile_group_id: nil)
|
|
72
|
+
params = {}
|
|
73
|
+
params[:trigger] = trigger if trigger
|
|
74
|
+
params[:status] = status if status
|
|
75
|
+
params[:page] = page if page
|
|
76
|
+
params[:per_page] = per_page if per_page
|
|
77
|
+
|
|
78
|
+
result = @client.request(:get, "/profiles/#{id}/post_syncs",
|
|
79
|
+
params: params.empty? ? nil : params,
|
|
80
|
+
profile_group_id: profile_group_id
|
|
81
|
+
)
|
|
82
|
+
syncs = (result[:data] || []).map { |s| PostSync.new(**s) }
|
|
83
|
+
PaginatedResponse.new(
|
|
84
|
+
data: syncs,
|
|
85
|
+
total: result[:total],
|
|
86
|
+
page: result[:page],
|
|
87
|
+
per_page: result[:per_page]
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fetches a single run. Poll this to follow a backfill to completion —
|
|
92
|
+
# the run is finished when `status` is "completed" or "failed".
|
|
93
|
+
def post_sync(id, post_sync_id, profile_group_id: nil)
|
|
94
|
+
result = @client.request(:get, "/profiles/#{id}/post_syncs/#{post_sync_id}",
|
|
95
|
+
profile_group_id: profile_group_id
|
|
96
|
+
)
|
|
97
|
+
PostSync.new(**result)
|
|
98
|
+
end
|
|
99
|
+
|
|
53
100
|
# Lists DM ice breakers. Supported for Instagram profiles only.
|
|
54
101
|
def ice_breakers(id, profile_group_id: nil)
|
|
55
102
|
result = @client.request(:get, "/profiles/#{id}/ice_breakers", profile_group_id: profile_group_id)
|
|
@@ -57,22 +104,29 @@ module PostProxy
|
|
|
57
104
|
end
|
|
58
105
|
|
|
59
106
|
# Replaces the DM ice breakers for a profile (1-4 items).
|
|
60
|
-
def set_ice_breakers(id, ice_breakers, profile_group_id: nil)
|
|
107
|
+
def set_ice_breakers(id, ice_breakers, profile_group_id: nil, idempotency_key: nil)
|
|
61
108
|
items = ice_breakers.map { |ib| ib.is_a?(IceBreaker) ? ib.to_h : ib }
|
|
62
109
|
result = @client.request(:post, "/profiles/#{id}/ice_breakers",
|
|
63
110
|
json: { ice_breakers: items },
|
|
64
|
-
profile_group_id: profile_group_id
|
|
111
|
+
profile_group_id: profile_group_id,
|
|
112
|
+
idempotency_key: idempotency_key
|
|
65
113
|
)
|
|
66
114
|
SuccessResponse.new(**result)
|
|
67
115
|
end
|
|
68
116
|
|
|
69
|
-
def delete_ice_breakers(id, profile_group_id: nil)
|
|
70
|
-
result = @client.request(:delete, "/profiles/#{id}/ice_breakers",
|
|
117
|
+
def delete_ice_breakers(id, profile_group_id: nil, idempotency_key: nil)
|
|
118
|
+
result = @client.request(:delete, "/profiles/#{id}/ice_breakers",
|
|
119
|
+
profile_group_id: profile_group_id,
|
|
120
|
+
idempotency_key: idempotency_key
|
|
121
|
+
)
|
|
71
122
|
SuccessResponse.new(**result)
|
|
72
123
|
end
|
|
73
124
|
|
|
74
|
-
def delete(id, profile_group_id: nil)
|
|
75
|
-
result = @client.request(:delete, "/profiles/#{id}",
|
|
125
|
+
def delete(id, profile_group_id: nil, idempotency_key: nil)
|
|
126
|
+
result = @client.request(:delete, "/profiles/#{id}",
|
|
127
|
+
profile_group_id: profile_group_id,
|
|
128
|
+
idempotency_key: idempotency_key
|
|
129
|
+
)
|
|
76
130
|
SuccessResponse.new(**result)
|
|
77
131
|
end
|
|
78
132
|
end
|
|
@@ -21,7 +21,8 @@ module PostProxy
|
|
|
21
21
|
NextSlotResponse.new(**result)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def create(name, profile_group_id:, description: nil, timezone: nil, jitter: nil, timeslots: nil
|
|
24
|
+
def create(name, profile_group_id:, description: nil, timezone: nil, jitter: nil, timeslots: nil,
|
|
25
|
+
idempotency_key: nil)
|
|
25
26
|
post_queue = { name: name }
|
|
26
27
|
post_queue[:description] = description if description
|
|
27
28
|
post_queue[:timezone] = timezone if timezone
|
|
@@ -33,11 +34,12 @@ module PostProxy
|
|
|
33
34
|
post_queue: post_queue,
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
result = @client.request(:post, "/post_queues", json: json_body)
|
|
37
|
+
result = @client.request(:post, "/post_queues", json: json_body, idempotency_key: idempotency_key)
|
|
37
38
|
Queue.new(**result)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def update(id, name: nil, description: nil, timezone: nil, enabled: nil, jitter: nil, timeslots: nil
|
|
41
|
+
def update(id, name: nil, description: nil, timezone: nil, enabled: nil, jitter: nil, timeslots: nil,
|
|
42
|
+
idempotency_key: nil)
|
|
41
43
|
post_queue = {}
|
|
42
44
|
post_queue[:name] = name unless name.nil?
|
|
43
45
|
post_queue[:description] = description unless description.nil?
|
|
@@ -48,12 +50,12 @@ module PostProxy
|
|
|
48
50
|
|
|
49
51
|
json_body = { post_queue: post_queue }
|
|
50
52
|
|
|
51
|
-
result = @client.request(:patch, "/post_queues/#{id}", json: json_body)
|
|
53
|
+
result = @client.request(:patch, "/post_queues/#{id}", json: json_body, idempotency_key: idempotency_key)
|
|
52
54
|
Queue.new(**result)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def delete(id)
|
|
56
|
-
result = @client.request(:delete, "/post_queues/#{id}")
|
|
57
|
+
def delete(id, idempotency_key: nil)
|
|
58
|
+
result = @client.request(:delete, "/post_queues/#{id}", idempotency_key: idempotency_key)
|
|
57
59
|
DeleteResponse.new(**result)
|
|
58
60
|
end
|
|
59
61
|
end
|
|
@@ -16,27 +16,27 @@ module PostProxy
|
|
|
16
16
|
Webhook.new(**result)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def create(url, events:, description: nil)
|
|
19
|
+
def create(url, events:, description: nil, idempotency_key: nil)
|
|
20
20
|
json_body = { url: url, events: events }
|
|
21
21
|
json_body[:description] = description if description
|
|
22
22
|
|
|
23
|
-
result = @client.request(:post, "/webhooks", json: json_body)
|
|
23
|
+
result = @client.request(:post, "/webhooks", json: json_body, idempotency_key: idempotency_key)
|
|
24
24
|
Webhook.new(**result)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def update(id, url: nil, events: nil, enabled: nil, description: nil)
|
|
27
|
+
def update(id, url: nil, events: nil, enabled: nil, description: nil, idempotency_key: nil)
|
|
28
28
|
json_body = {}
|
|
29
29
|
json_body[:url] = url unless url.nil?
|
|
30
30
|
json_body[:events] = events unless events.nil?
|
|
31
31
|
json_body[:enabled] = enabled unless enabled.nil?
|
|
32
32
|
json_body[:description] = description unless description.nil?
|
|
33
33
|
|
|
34
|
-
result = @client.request(:patch, "/webhooks/#{id}", json: json_body)
|
|
34
|
+
result = @client.request(:patch, "/webhooks/#{id}", json: json_body, idempotency_key: idempotency_key)
|
|
35
35
|
Webhook.new(**result)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def delete(id)
|
|
39
|
-
result = @client.request(:delete, "/webhooks/#{id}")
|
|
38
|
+
def delete(id, idempotency_key: nil)
|
|
39
|
+
result = @client.request(:delete, "/webhooks/#{id}", idempotency_key: idempotency_key)
|
|
40
40
|
DeleteResponse.new(**result)
|
|
41
41
|
end
|
|
42
42
|
|
data/lib/postproxy/types.rb
CHANGED
|
@@ -261,10 +261,13 @@ module PostProxy
|
|
|
261
261
|
end
|
|
262
262
|
|
|
263
263
|
class StatsRecord < Model
|
|
264
|
-
|
|
264
|
+
# `raw_stats` carries every metric under its original platform name, e.g.
|
|
265
|
+
# `views` for Instagram or `impression_count` for Twitter/X.
|
|
266
|
+
attr_accessor :stats, :raw_stats, :recorded_at
|
|
265
267
|
|
|
266
268
|
def initialize(**attrs)
|
|
267
269
|
@stats = {}
|
|
270
|
+
@raw_stats = {}
|
|
268
271
|
@recorded_at = nil
|
|
269
272
|
super
|
|
270
273
|
@recorded_at = parse_time(@recorded_at)
|
|
@@ -426,11 +429,94 @@ module PostProxy
|
|
|
426
429
|
end
|
|
427
430
|
end
|
|
428
431
|
|
|
432
|
+
# A tappable chip above the participant's composer, gone once tapped.
|
|
433
|
+
# Facebook and Instagram only; up to 13 per send. +content_type+ is optional
|
|
434
|
+
# on send (only "text" is accepted) and always present on responses.
|
|
435
|
+
class QuickReply < Model
|
|
436
|
+
attr_accessor :content_type, :title, :payload
|
|
437
|
+
|
|
438
|
+
def initialize(**attrs)
|
|
439
|
+
@content_type = nil
|
|
440
|
+
super
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def to_h
|
|
444
|
+
{ content_type: @content_type, title: @title, payload: @payload }.compact
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# A button attached to the message, delivered as a Meta generic template.
|
|
449
|
+
# Facebook and Instagram only; up to 3 per send. +url+ is required and must be
|
|
450
|
+
# https when +type+ is "web_url"; +payload+ is required when +type+ is
|
|
451
|
+
# "postback". +type+ is a plain string rather than an enum so a new Meta
|
|
452
|
+
# button type needs no SDK release.
|
|
453
|
+
class MessageButton < Model
|
|
454
|
+
attr_accessor :type, :title, :url, :payload
|
|
455
|
+
|
|
456
|
+
def initialize(**attrs)
|
|
457
|
+
@url = nil
|
|
458
|
+
@payload = nil
|
|
459
|
+
super
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def to_h
|
|
463
|
+
{ type: @type, title: @title, url: @url, payload: @payload }.compact
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
class CardDefaultAction < Model
|
|
468
|
+
attr_accessor :type, :url
|
|
469
|
+
|
|
470
|
+
def to_h
|
|
471
|
+
{ type: @type, url: @url }.compact
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Extra fields for the generic-template element that carries +buttons+.
|
|
476
|
+
# Requires +buttons+. +subtitle+ is capped at 80 characters, and both URLs
|
|
477
|
+
# must be https.
|
|
478
|
+
class MessageCard < Model
|
|
479
|
+
attr_accessor :subtitle, :image_url, :default_action
|
|
480
|
+
|
|
481
|
+
def initialize(**attrs)
|
|
482
|
+
@subtitle = nil
|
|
483
|
+
@image_url = nil
|
|
484
|
+
@default_action = nil
|
|
485
|
+
super
|
|
486
|
+
if @default_action && !@default_action.is_a?(CardDefaultAction)
|
|
487
|
+
@default_action = CardDefaultAction.new(**@default_action.transform_keys(&:to_sym))
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def to_h
|
|
492
|
+
{
|
|
493
|
+
subtitle: @subtitle,
|
|
494
|
+
image_url: @image_url,
|
|
495
|
+
default_action: @default_action&.to_h
|
|
496
|
+
}.compact
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Set on inbound messages created by a tap on an element you sent. Derived
|
|
501
|
+
# from platform_data rather than stored, so it also resolves for taps ingested
|
|
502
|
+
# before PostProxy exposed this field. +kind+ is one of "quick_reply",
|
|
503
|
+
# "postback", or "callback_query" — the last is Telegram, so this is not
|
|
504
|
+
# Meta-only even though the send params are.
|
|
505
|
+
class TappedAction < Model
|
|
506
|
+
attr_accessor :kind, :payload, :title
|
|
507
|
+
|
|
508
|
+
def initialize(**attrs)
|
|
509
|
+
@title = nil
|
|
510
|
+
super
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
429
514
|
class Message < Model
|
|
430
515
|
attr_accessor :id, :chat_id, :external_id, :direction, :body, :status,
|
|
431
516
|
:tag, :external_comment_id, :error_message, :platform_data,
|
|
432
517
|
:external_posted_at, :external_delivered_at, :external_read_at,
|
|
433
518
|
:external_edited_at, :reply_to_external_id, :reply_markup,
|
|
519
|
+
:quick_replies, :buttons, :card, :tapped_action,
|
|
434
520
|
:external_deleted_at, :reactions, :attachments,
|
|
435
521
|
:is_unsupported, :created_at
|
|
436
522
|
|
|
@@ -447,6 +533,10 @@ module PostProxy
|
|
|
447
533
|
@external_edited_at = nil
|
|
448
534
|
@reply_to_external_id = nil
|
|
449
535
|
@reply_markup = nil
|
|
536
|
+
@quick_replies = nil
|
|
537
|
+
@buttons = nil
|
|
538
|
+
@card = nil
|
|
539
|
+
@tapped_action = nil
|
|
450
540
|
@external_deleted_at = nil
|
|
451
541
|
@reactions = []
|
|
452
542
|
@attachments = []
|
|
@@ -464,6 +554,20 @@ module PostProxy
|
|
|
464
554
|
@attachments = (@attachments || []).map do |a|
|
|
465
555
|
a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
|
|
466
556
|
end
|
|
557
|
+
# Left nil rather than [] when absent — the API omits these on non-Meta
|
|
558
|
+
# networks, and an empty array would read as "sent with none".
|
|
559
|
+
@quick_replies = @quick_replies&.map do |q|
|
|
560
|
+
q.is_a?(QuickReply) ? q : QuickReply.new(**q.transform_keys(&:to_sym))
|
|
561
|
+
end
|
|
562
|
+
@buttons = @buttons&.map do |b|
|
|
563
|
+
b.is_a?(MessageButton) ? b : MessageButton.new(**b.transform_keys(&:to_sym))
|
|
564
|
+
end
|
|
565
|
+
if @card && !@card.is_a?(MessageCard)
|
|
566
|
+
@card = MessageCard.new(**@card.transform_keys(&:to_sym))
|
|
567
|
+
end
|
|
568
|
+
if @tapped_action && !@tapped_action.is_a?(TappedAction)
|
|
569
|
+
@tapped_action = TappedAction.new(**@tapped_action.transform_keys(&:to_sym))
|
|
570
|
+
end
|
|
467
571
|
end
|
|
468
572
|
|
|
469
573
|
private
|
|
@@ -505,6 +609,79 @@ module PostProxy
|
|
|
505
609
|
attr_accessor :accepted
|
|
506
610
|
end
|
|
507
611
|
|
|
612
|
+
# A comment from Comments#list_all. Flat: replies are their own entries
|
|
613
|
+
# linked to their parent by `parent_external_id` rather than nested under
|
|
614
|
+
# `replies`.
|
|
615
|
+
class BulkComment < Model
|
|
616
|
+
attr_accessor :post_id, :profile_id, :platform, :id, :external_id, :body,
|
|
617
|
+
:status, :author_username, :author_avatar_url,
|
|
618
|
+
:author_external_id, :metadata, :parent_external_id,
|
|
619
|
+
:like_count, :is_hidden, :permalink, :platform_data,
|
|
620
|
+
:attachments, :posted_at, :created_at
|
|
621
|
+
|
|
622
|
+
def initialize(**attrs)
|
|
623
|
+
@external_id = nil
|
|
624
|
+
@author_username = nil
|
|
625
|
+
@author_avatar_url = nil
|
|
626
|
+
@author_external_id = nil
|
|
627
|
+
@metadata = nil
|
|
628
|
+
@parent_external_id = nil
|
|
629
|
+
@like_count = 0
|
|
630
|
+
@is_hidden = false
|
|
631
|
+
@permalink = nil
|
|
632
|
+
@platform_data = nil
|
|
633
|
+
@attachments = []
|
|
634
|
+
@posted_at = nil
|
|
635
|
+
super
|
|
636
|
+
@posted_at = parse_time(@posted_at)
|
|
637
|
+
@created_at = parse_time(@created_at)
|
|
638
|
+
@attachments = (@attachments || []).map do |a|
|
|
639
|
+
a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
private
|
|
644
|
+
|
|
645
|
+
def parse_time(value)
|
|
646
|
+
return nil if value.nil?
|
|
647
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# A record of one post pull for a profile — the sync fired when the profile
|
|
652
|
+
# connects, the recurring poll, or a backfill.
|
|
653
|
+
class PostSync < Model
|
|
654
|
+
attr_accessor :id, :profile_id, :kind, :trigger, :status, :started_at,
|
|
655
|
+
:completed_at, :posts_seen, :posts_imported, :backfill_from,
|
|
656
|
+
:oldest_posted_at, :error, :created_at
|
|
657
|
+
|
|
658
|
+
def initialize(**attrs)
|
|
659
|
+
@started_at = nil
|
|
660
|
+
@completed_at = nil
|
|
661
|
+
@posts_seen = 0
|
|
662
|
+
# Posts that were new and got created — lower than `posts_seen` whenever
|
|
663
|
+
# the run re-read posts you already have.
|
|
664
|
+
@posts_imported = 0
|
|
665
|
+
@backfill_from = nil
|
|
666
|
+
# Publish date of the oldest post the run reached.
|
|
667
|
+
@oldest_posted_at = nil
|
|
668
|
+
@error = nil
|
|
669
|
+
super
|
|
670
|
+
@started_at = parse_time(@started_at)
|
|
671
|
+
@completed_at = parse_time(@completed_at)
|
|
672
|
+
@backfill_from = parse_time(@backfill_from)
|
|
673
|
+
@oldest_posted_at = parse_time(@oldest_posted_at)
|
|
674
|
+
@created_at = parse_time(@created_at)
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
private
|
|
678
|
+
|
|
679
|
+
def parse_time(value)
|
|
680
|
+
return nil if value.nil?
|
|
681
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
508
685
|
class DeleteResponse < Model
|
|
509
686
|
attr_accessor :deleted
|
|
510
687
|
end
|
|
@@ -589,9 +766,37 @@ module PostProxy
|
|
|
589
766
|
attr_accessor :format, :title, :first_comment, :page_id
|
|
590
767
|
end
|
|
591
768
|
|
|
769
|
+
# An Instagram account to tag in a post. Images require `x` and `y`; reels
|
|
770
|
+
# and video slides are tagged by username only — Instagram ignores
|
|
771
|
+
# coordinates there. `media_index` picks the carousel slide (0-based).
|
|
772
|
+
class InstagramUserTag < Model
|
|
773
|
+
attr_accessor :username, :x, :y, :media_index
|
|
774
|
+
end
|
|
775
|
+
|
|
592
776
|
class InstagramParams < Model
|
|
593
777
|
attr_accessor :format, :first_comment, :collaborators, :cover_url,
|
|
594
|
-
:audio_name, :trial_strategy, :thumb_offset
|
|
778
|
+
:audio_name, :trial_strategy, :thumb_offset, :user_tags
|
|
779
|
+
|
|
780
|
+
def initialize(**attrs)
|
|
781
|
+
@user_tags = nil
|
|
782
|
+
super
|
|
783
|
+
@user_tags = @user_tags&.map do |t|
|
|
784
|
+
t.is_a?(InstagramUserTag) ? t : InstagramUserTag.new(**t.transform_keys(&:to_sym))
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# User tags must reach the wire as plain hashes, with unset coordinates
|
|
789
|
+
# dropped rather than sent as nulls.
|
|
790
|
+
def to_h
|
|
791
|
+
result = super
|
|
792
|
+
tags = result[:user_tags]
|
|
793
|
+
return result if tags.nil?
|
|
794
|
+
|
|
795
|
+
result[:user_tags] = tags.map do |t|
|
|
796
|
+
(t.is_a?(Model) ? t.to_h : t).reject { |_, v| v.nil? }
|
|
797
|
+
end
|
|
798
|
+
result
|
|
799
|
+
end
|
|
595
800
|
end
|
|
596
801
|
|
|
597
802
|
class TikTokParams < Model
|
data/lib/postproxy/version.rb
CHANGED