postproxy-sdk 1.10.0 → 1.12.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 +217 -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 +23 -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 +91 -2
- data/lib/postproxy/resources/queues.rb +8 -6
- data/lib/postproxy/resources/webhooks.rb +6 -6
- data/lib/postproxy/types.rb +132 -4
- 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: 9fabdc6a06ca3cef409cf88533c925ff183e3e1c7089fd53aa541c09bff0aa6f
|
|
4
|
+
data.tar.gz: 7b4245a53e37aab93f006d408430676dbe3352a8815e5479de765946d941e20e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97e32211fa2f54a2a28b506333108a484153083efcf9272bcf23fc821e27e6e2e88be7dd7d166acf6f9cd339a69a07afd1fb9b9f3c271a860b8ec9f5b41b7a9e
|
|
7
|
+
data.tar.gz: 3217e3f75dd540a6c13215dc5a5b7830d483ebd38f4cd30433162e7c732f69b214b06e3f73ccfbe5683f953a9c57133bf9dd1de4bdaf4ffe5dc3a631c15a132e
|
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`).
|
|
@@ -431,6 +502,24 @@ profile = client.profiles.get("prof-id")
|
|
|
431
502
|
# Get placements for a profile
|
|
432
503
|
placements = client.profiles.placements("prof-id").data
|
|
433
504
|
|
|
505
|
+
# Move a placement (e.g. a Facebook Page or Telegram channel) to another group
|
|
506
|
+
placement = client.profiles.assign_placement_to_group("prof-id",
|
|
507
|
+
placement_id: "placement-external-id",
|
|
508
|
+
target_profile_group_id: "pg-other"
|
|
509
|
+
)
|
|
510
|
+
puts placement.profile_group_id # => "pg-other"
|
|
511
|
+
|
|
512
|
+
# Ice breakers (Instagram DMs): FAQ prompts shown when a user opens a chat
|
|
513
|
+
result = client.profiles.ice_breakers("prof-id")
|
|
514
|
+
puts result.ice_breakers.map(&:question)
|
|
515
|
+
|
|
516
|
+
client.profiles.set_ice_breakers("prof-id", [
|
|
517
|
+
{ question: "What services do you offer?", payload: "services" },
|
|
518
|
+
{ question: "What are your hours?", payload: "hours" }
|
|
519
|
+
]) # 1-4 items
|
|
520
|
+
|
|
521
|
+
client.profiles.delete_ice_breakers("prof-id")
|
|
522
|
+
|
|
434
523
|
# Delete a profile
|
|
435
524
|
client.profiles.delete("prof-id")
|
|
436
525
|
|
|
@@ -448,6 +537,81 @@ bsky = client.profiles.get_profile_stats("prof_bsky_001")
|
|
|
448
537
|
puts bsky.data.records.last.stats[:followersCount]
|
|
449
538
|
```
|
|
450
539
|
|
|
540
|
+
Every stats record (post stats and profile stats alike) carries `raw_stats` alongside the
|
|
541
|
+
normalized `stats`, exposing each metric under its **original platform name**:
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
stats = client.posts.stats(["post-id"])
|
|
545
|
+
record = stats.data["post-id"].platforms.first.records.first
|
|
546
|
+
|
|
547
|
+
puts record.stats[:impressions] # normalized
|
|
548
|
+
puts record.raw_stats[:views] # Instagram's own name
|
|
549
|
+
puts record.raw_stats[:impression_count] # Twitter/X's own name
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
LinkedIn post stats now normalize `likes`, `comments`, `shares`, and `clicks` alongside
|
|
553
|
+
`impressions` — previously only `impressions` was normalized.
|
|
554
|
+
|
|
555
|
+
### Post syncs & backfill
|
|
556
|
+
|
|
557
|
+
PostProxy mirrors posts published natively on a platform into your account. Every one of
|
|
558
|
+
those pulls is recorded as a **post sync**: the one fired when the profile connects, the
|
|
559
|
+
recurring poll, and any backfill you start.
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
# Start a backfill — walks the feed backwards from the newest post in batches
|
|
563
|
+
# of 25 until it reaches `from` or the platform stops returning posts.
|
|
564
|
+
sync = client.profiles.backfill_posts("prof-id", from: "2025-01-01")
|
|
565
|
+
puts sync.id, sync.status # => "sync456def" "pending"
|
|
566
|
+
|
|
567
|
+
# Poll it to completion — finished when status is "completed" or "failed"
|
|
568
|
+
run = client.profiles.post_sync("prof-id", sync.id)
|
|
569
|
+
puts "#{run.posts_imported} of #{run.posts_seen}, back to #{run.oldest_posted_at}"
|
|
570
|
+
|
|
571
|
+
# List recent runs (kept for 30 days), newest first
|
|
572
|
+
runs = client.profiles.post_syncs("prof-id",
|
|
573
|
+
trigger: "backfill", # connect | scheduled | backfill
|
|
574
|
+
status: "completed", # pending | running | completed | failed
|
|
575
|
+
per_page: 25
|
|
576
|
+
)
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
| `PostSync` field | Description |
|
|
580
|
+
|---|---|
|
|
581
|
+
| `id` | Sync identifier |
|
|
582
|
+
| `profile_id` | Profile this run belongs to |
|
|
583
|
+
| `kind` | Always `posts` today |
|
|
584
|
+
| `trigger` | `connect`, `scheduled`, or `backfill` |
|
|
585
|
+
| `status` | `pending`, `running`, `completed`, or `failed` |
|
|
586
|
+
| `started_at` / `completed_at` | `Time` or `nil` |
|
|
587
|
+
| `posts_seen` | Posts the platform returned across the run |
|
|
588
|
+
| `posts_imported` | Posts that were **new** and got created |
|
|
589
|
+
| `backfill_from` | The date floor requested; `nil` for `connect`/`scheduled` |
|
|
590
|
+
| `oldest_posted_at` | Publish date of the oldest post the run reached |
|
|
591
|
+
| `error` | Platform error message when `status` is `"failed"` |
|
|
592
|
+
| `created_at` | `Time` |
|
|
593
|
+
|
|
594
|
+
**How far back a backfill reaches depends on the platform's API**, not on PostProxy: where
|
|
595
|
+
history is pageable we follow it, otherwise the run ends early with whatever it got and
|
|
596
|
+
still reports `status == "completed"`.
|
|
597
|
+
|
|
598
|
+
Only one backfill runs per profile at a time — starting a second raises `ConflictError`
|
|
599
|
+
carrying the running one's id:
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
begin
|
|
603
|
+
client.profiles.backfill_posts("prof-id", from: "2025-01-01")
|
|
604
|
+
rescue PostProxy::ConflictError => e
|
|
605
|
+
running_id = e.response[:profile_sync_id]
|
|
606
|
+
# Poll the run that's already going.
|
|
607
|
+
end
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Posts you already have are skipped, so overlapping backfills are safe. Imported posts
|
|
611
|
+
behave exactly like ones the poll picks up (`source: "imported"`, `post.imported`
|
|
612
|
+
webhook), but a backfill's follow-up work is queued at a lower priority so a deep run
|
|
613
|
+
can't slow down publishing.
|
|
614
|
+
|
|
451
615
|
## Profile Groups
|
|
452
616
|
|
|
453
617
|
```ruby
|
|
@@ -521,6 +685,9 @@ platforms = PostProxy::PlatformParams.new(
|
|
|
521
685
|
board_id: "board-123"
|
|
522
686
|
),
|
|
523
687
|
threads: PostProxy::ThreadsParams.new(format: "post"),
|
|
688
|
+
# Twitter also supports polls:
|
|
689
|
+
# twitter: PostProxy::TwitterParams.new(format: "poll",
|
|
690
|
+
# poll_options: ["Yes", "No"], poll_duration_minutes: 1440),
|
|
524
691
|
twitter: PostProxy::TwitterParams.new(format: "post"),
|
|
525
692
|
bluesky: PostProxy::BlueskyParams.new(format: "post"),
|
|
526
693
|
telegram: PostProxy::TelegramParams.new(
|
|
@@ -539,6 +706,43 @@ post = client.posts.create(
|
|
|
539
706
|
|
|
540
707
|
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)`.
|
|
541
708
|
|
|
709
|
+
### Instagram user tags
|
|
710
|
+
|
|
711
|
+
Tag public Instagram accounts in a post — feed post, reel, or story:
|
|
712
|
+
|
|
713
|
+
```ruby
|
|
714
|
+
client.posts.create(
|
|
715
|
+
"Shot on location",
|
|
716
|
+
profiles: ["ig-profile-id"],
|
|
717
|
+
media: [
|
|
718
|
+
"https://example.com/1.jpg",
|
|
719
|
+
"https://example.com/2.jpg",
|
|
720
|
+
"https://example.com/3.mp4"
|
|
721
|
+
],
|
|
722
|
+
platforms: PostProxy::PlatformParams.new(
|
|
723
|
+
instagram: PostProxy::InstagramParams.new(
|
|
724
|
+
format: "post",
|
|
725
|
+
user_tags: [
|
|
726
|
+
{ username: "natgeo", x: 0.5, y: 0.4 }, # slide 0
|
|
727
|
+
{ username: "nasa", x: 0.2, y: 0.8, media_index: 1 }, # slide 1
|
|
728
|
+
{ username: "spacex", media_index: 2 } # video — username only
|
|
729
|
+
]
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
- **Images require `x` and `y`** — floats `0.0`–`1.0` measured from the top-left corner.
|
|
736
|
+
- **Reels and video slides** are tagged by username only; coordinates are ignored and dropped.
|
|
737
|
+
- **Stories** accept coordinates but don't need them.
|
|
738
|
+
- `media_index` picks the carousel slide (0-based, defaults to `0`, video slides included).
|
|
739
|
+
- A leading `@` on a username is stripped for you.
|
|
740
|
+
|
|
741
|
+
Coordinates outside `0.0`–`1.0`, a `media_index` past the last media item, or an image tag
|
|
742
|
+
missing `x`/`y` are rejected with a `ValidationError` naming the offending entry. Accounts
|
|
743
|
+
that are private or have tagging turned off are silently skipped by Instagram at publish
|
|
744
|
+
time.
|
|
745
|
+
|
|
542
746
|
### Google Business
|
|
543
747
|
|
|
544
748
|
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).
|
|
@@ -568,6 +772,10 @@ rescue PostProxy::AuthenticationError => e
|
|
|
568
772
|
puts "Auth failed: #{e.message}" # 401
|
|
569
773
|
rescue PostProxy::NotFoundError => e
|
|
570
774
|
puts "Not found: #{e.message}" # 404
|
|
775
|
+
rescue PostProxy::ConflictError => e
|
|
776
|
+
puts "Conflict: #{e.message}" # 409
|
|
777
|
+
puts e.response[:duplicate_post_id] # on a duplicate post
|
|
778
|
+
puts e.response[:profile_sync_id] # on a backfill already running
|
|
571
779
|
rescue PostProxy::ValidationError => e
|
|
572
780
|
puts "Invalid: #{e.message}" # 422
|
|
573
781
|
rescue PostProxy::BadRequestError => e
|
|
@@ -578,6 +786,15 @@ rescue PostProxy::Error => e
|
|
|
578
786
|
end
|
|
579
787
|
```
|
|
580
788
|
|
|
789
|
+
| Status | Error | Raised for |
|
|
790
|
+
|---|---|---|
|
|
791
|
+
| 400 | `BadRequestError` | Missing required parameters |
|
|
792
|
+
| 401 | `AuthenticationError` | Invalid, missing, or insufficient API key permissions |
|
|
793
|
+
| 404 | `NotFoundError` | Resource does not exist or is not accessible |
|
|
794
|
+
| 409 | `ConflictError` | Duplicate submission, a backfill already running, or an in-flight `Idempotency-Key` |
|
|
795
|
+
| 422 | `ValidationError` | Validation failed |
|
|
796
|
+
| 429 | `Error` | Posting rate limit reached |
|
|
797
|
+
|
|
581
798
|
## License
|
|
582
799
|
|
|
583
800
|
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
|
|
@@ -23,7 +23,8 @@ module PostProxy
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def send_message(chat_id, body: nil, media: nil, media_files: nil, tag: nil,
|
|
26
|
-
reply_to_external_id: nil, reply_markup: nil, profile_group_id: nil
|
|
26
|
+
reply_to_external_id: nil, reply_markup: nil, profile_group_id: nil,
|
|
27
|
+
idempotency_key: nil)
|
|
27
28
|
has_files = media_files && !media_files.empty?
|
|
28
29
|
|
|
29
30
|
if has_files
|
|
@@ -47,7 +48,8 @@ module PostProxy
|
|
|
47
48
|
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
48
49
|
data: form_data,
|
|
49
50
|
files: files,
|
|
50
|
-
profile_group_id: profile_group_id
|
|
51
|
+
profile_group_id: profile_group_id,
|
|
52
|
+
idempotency_key: idempotency_key
|
|
51
53
|
)
|
|
52
54
|
else
|
|
53
55
|
json_body = {}
|
|
@@ -57,7 +59,11 @@ module PostProxy
|
|
|
57
59
|
json_body[:reply_to_external_id] = reply_to_external_id if reply_to_external_id
|
|
58
60
|
json_body[:reply_markup] = reply_markup if reply_markup
|
|
59
61
|
|
|
60
|
-
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
62
|
+
result = @client.request(:post, "/chats/#{chat_id}/messages",
|
|
63
|
+
json: json_body,
|
|
64
|
+
profile_group_id: profile_group_id,
|
|
65
|
+
idempotency_key: idempotency_key
|
|
66
|
+
)
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
Message.new(**result)
|
|
@@ -69,29 +75,37 @@ module PostProxy
|
|
|
69
75
|
Message.new(**result)
|
|
70
76
|
end
|
|
71
77
|
|
|
72
|
-
def edit(message_id, body: nil, reply_markup: nil, profile_group_id: nil)
|
|
78
|
+
def edit(message_id, body: nil, reply_markup: nil, profile_group_id: nil, idempotency_key: nil)
|
|
73
79
|
json_body = {}
|
|
74
80
|
json_body[:body] = body if body
|
|
75
81
|
json_body[:reply_markup] = reply_markup if reply_markup
|
|
76
82
|
|
|
77
|
-
result = @client.request(:patch, "/messages/#{message_id}",
|
|
83
|
+
result = @client.request(:patch, "/messages/#{message_id}",
|
|
84
|
+
json: json_body,
|
|
85
|
+
profile_group_id: profile_group_id,
|
|
86
|
+
idempotency_key: idempotency_key
|
|
87
|
+
)
|
|
78
88
|
Message.new(**result)
|
|
79
89
|
end
|
|
80
90
|
|
|
81
|
-
def react(message_id, reaction: nil, emoji: nil, profile_group_id: nil)
|
|
91
|
+
def react(message_id, reaction: nil, emoji: nil, profile_group_id: nil, idempotency_key: nil)
|
|
82
92
|
json_body = {}
|
|
83
93
|
json_body[:reaction] = reaction if reaction
|
|
84
94
|
json_body[:emoji] = emoji if emoji
|
|
85
95
|
|
|
86
96
|
result = @client.request(:post, "/messages/#{message_id}/react",
|
|
87
97
|
json: json_body.empty? ? nil : json_body,
|
|
88
|
-
profile_group_id: profile_group_id
|
|
98
|
+
profile_group_id: profile_group_id,
|
|
99
|
+
idempotency_key: idempotency_key
|
|
89
100
|
)
|
|
90
101
|
Message.new(**result)
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
def unreact(message_id, profile_group_id: nil)
|
|
94
|
-
result = @client.request(:delete, "/messages/#{message_id}/unreact",
|
|
104
|
+
def unreact(message_id, profile_group_id: nil, idempotency_key: nil)
|
|
105
|
+
result = @client.request(:delete, "/messages/#{message_id}/unreact",
|
|
106
|
+
profile_group_id: profile_group_id,
|
|
107
|
+
idempotency_key: idempotency_key
|
|
108
|
+
)
|
|
95
109
|
Message.new(**result)
|
|
96
110
|
end
|
|
97
111
|
|
|
@@ -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
|
|
|
@@ -36,8 +36,97 @@ module PostProxy
|
|
|
36
36
|
ProfileStatsResponse.new(data: result[:data])
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# Moves a placement (e.g. a Facebook Page or Telegram channel) to another
|
|
40
|
+
# profile group. `placement_id` is the placement's external ID as
|
|
41
|
+
# returned by #placements.
|
|
42
|
+
def assign_placement_to_group(id, placement_id:, target_profile_group_id:, profile_group_id: nil,
|
|
43
|
+
idempotency_key: nil)
|
|
44
|
+
result = @client.request(:patch, "/profiles/#{id}/assign_placement_to_group",
|
|
45
|
+
json: {
|
|
46
|
+
placement_id: placement_id,
|
|
47
|
+
target_profile_group_id: target_profile_group_id
|
|
48
|
+
},
|
|
49
|
+
profile_group_id: profile_group_id,
|
|
50
|
+
idempotency_key: idempotency_key
|
|
51
|
+
)
|
|
52
|
+
Placement.new(**result)
|
|
53
|
+
end
|
|
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
|
+
|
|
100
|
+
# Lists DM ice breakers. Supported for Instagram profiles only.
|
|
101
|
+
def ice_breakers(id, profile_group_id: nil)
|
|
102
|
+
result = @client.request(:get, "/profiles/#{id}/ice_breakers", profile_group_id: profile_group_id)
|
|
103
|
+
IceBreakersResponse.new(**result)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Replaces the DM ice breakers for a profile (1-4 items).
|
|
107
|
+
def set_ice_breakers(id, ice_breakers, profile_group_id: nil, idempotency_key: nil)
|
|
108
|
+
items = ice_breakers.map { |ib| ib.is_a?(IceBreaker) ? ib.to_h : ib }
|
|
109
|
+
result = @client.request(:post, "/profiles/#{id}/ice_breakers",
|
|
110
|
+
json: { ice_breakers: items },
|
|
111
|
+
profile_group_id: profile_group_id,
|
|
112
|
+
idempotency_key: idempotency_key
|
|
113
|
+
)
|
|
114
|
+
SuccessResponse.new(**result)
|
|
115
|
+
end
|
|
116
|
+
|
|
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
|
+
)
|
|
122
|
+
SuccessResponse.new(**result)
|
|
123
|
+
end
|
|
124
|
+
|
|
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
|
+
)
|
|
41
130
|
SuccessResponse.new(**result)
|
|
42
131
|
end
|
|
43
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
|
@@ -235,14 +235,39 @@ module PostProxy
|
|
|
235
235
|
end
|
|
236
236
|
|
|
237
237
|
class Placement < Model
|
|
238
|
-
|
|
238
|
+
# `metadata` and `profile_group_id` are present in placements and
|
|
239
|
+
# assign_placement_to_group responses.
|
|
240
|
+
attr_accessor :id, :name, :metadata, :profile_group_id
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Instagram DM ice breaker (FAQ prompt). May carry extra platform fields.
|
|
244
|
+
class IceBreaker < Model
|
|
245
|
+
attr_accessor :question, :payload
|
|
246
|
+
|
|
247
|
+
def to_h
|
|
248
|
+
{ question: question, payload: payload }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class IceBreakersResponse < Model
|
|
253
|
+
attr_accessor :ice_breakers
|
|
254
|
+
|
|
255
|
+
def initialize(**attrs)
|
|
256
|
+
@ice_breakers = (attrs.delete(:ice_breakers) || []).map do |ib|
|
|
257
|
+
ib.is_a?(IceBreaker) ? ib : IceBreaker.new(**ib)
|
|
258
|
+
end
|
|
259
|
+
super
|
|
260
|
+
end
|
|
239
261
|
end
|
|
240
262
|
|
|
241
263
|
class StatsRecord < Model
|
|
242
|
-
|
|
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
|
|
243
267
|
|
|
244
268
|
def initialize(**attrs)
|
|
245
269
|
@stats = {}
|
|
270
|
+
@raw_stats = {}
|
|
246
271
|
@recorded_at = nil
|
|
247
272
|
super
|
|
248
273
|
@recorded_at = parse_time(@recorded_at)
|
|
@@ -483,6 +508,79 @@ module PostProxy
|
|
|
483
508
|
attr_accessor :accepted
|
|
484
509
|
end
|
|
485
510
|
|
|
511
|
+
# A comment from Comments#list_all. Flat: replies are their own entries
|
|
512
|
+
# linked to their parent by `parent_external_id` rather than nested under
|
|
513
|
+
# `replies`.
|
|
514
|
+
class BulkComment < Model
|
|
515
|
+
attr_accessor :post_id, :profile_id, :platform, :id, :external_id, :body,
|
|
516
|
+
:status, :author_username, :author_avatar_url,
|
|
517
|
+
:author_external_id, :metadata, :parent_external_id,
|
|
518
|
+
:like_count, :is_hidden, :permalink, :platform_data,
|
|
519
|
+
:attachments, :posted_at, :created_at
|
|
520
|
+
|
|
521
|
+
def initialize(**attrs)
|
|
522
|
+
@external_id = nil
|
|
523
|
+
@author_username = nil
|
|
524
|
+
@author_avatar_url = nil
|
|
525
|
+
@author_external_id = nil
|
|
526
|
+
@metadata = nil
|
|
527
|
+
@parent_external_id = nil
|
|
528
|
+
@like_count = 0
|
|
529
|
+
@is_hidden = false
|
|
530
|
+
@permalink = nil
|
|
531
|
+
@platform_data = nil
|
|
532
|
+
@attachments = []
|
|
533
|
+
@posted_at = nil
|
|
534
|
+
super
|
|
535
|
+
@posted_at = parse_time(@posted_at)
|
|
536
|
+
@created_at = parse_time(@created_at)
|
|
537
|
+
@attachments = (@attachments || []).map do |a|
|
|
538
|
+
a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
private
|
|
543
|
+
|
|
544
|
+
def parse_time(value)
|
|
545
|
+
return nil if value.nil?
|
|
546
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# A record of one post pull for a profile — the sync fired when the profile
|
|
551
|
+
# connects, the recurring poll, or a backfill.
|
|
552
|
+
class PostSync < Model
|
|
553
|
+
attr_accessor :id, :profile_id, :kind, :trigger, :status, :started_at,
|
|
554
|
+
:completed_at, :posts_seen, :posts_imported, :backfill_from,
|
|
555
|
+
:oldest_posted_at, :error, :created_at
|
|
556
|
+
|
|
557
|
+
def initialize(**attrs)
|
|
558
|
+
@started_at = nil
|
|
559
|
+
@completed_at = nil
|
|
560
|
+
@posts_seen = 0
|
|
561
|
+
# Posts that were new and got created — lower than `posts_seen` whenever
|
|
562
|
+
# the run re-read posts you already have.
|
|
563
|
+
@posts_imported = 0
|
|
564
|
+
@backfill_from = nil
|
|
565
|
+
# Publish date of the oldest post the run reached.
|
|
566
|
+
@oldest_posted_at = nil
|
|
567
|
+
@error = nil
|
|
568
|
+
super
|
|
569
|
+
@started_at = parse_time(@started_at)
|
|
570
|
+
@completed_at = parse_time(@completed_at)
|
|
571
|
+
@backfill_from = parse_time(@backfill_from)
|
|
572
|
+
@oldest_posted_at = parse_time(@oldest_posted_at)
|
|
573
|
+
@created_at = parse_time(@created_at)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
private
|
|
577
|
+
|
|
578
|
+
def parse_time(value)
|
|
579
|
+
return nil if value.nil?
|
|
580
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
486
584
|
class DeleteResponse < Model
|
|
487
585
|
attr_accessor :deleted
|
|
488
586
|
end
|
|
@@ -567,9 +665,37 @@ module PostProxy
|
|
|
567
665
|
attr_accessor :format, :title, :first_comment, :page_id
|
|
568
666
|
end
|
|
569
667
|
|
|
668
|
+
# An Instagram account to tag in a post. Images require `x` and `y`; reels
|
|
669
|
+
# and video slides are tagged by username only — Instagram ignores
|
|
670
|
+
# coordinates there. `media_index` picks the carousel slide (0-based).
|
|
671
|
+
class InstagramUserTag < Model
|
|
672
|
+
attr_accessor :username, :x, :y, :media_index
|
|
673
|
+
end
|
|
674
|
+
|
|
570
675
|
class InstagramParams < Model
|
|
571
676
|
attr_accessor :format, :first_comment, :collaborators, :cover_url,
|
|
572
|
-
:audio_name, :trial_strategy, :thumb_offset
|
|
677
|
+
:audio_name, :trial_strategy, :thumb_offset, :user_tags
|
|
678
|
+
|
|
679
|
+
def initialize(**attrs)
|
|
680
|
+
@user_tags = nil
|
|
681
|
+
super
|
|
682
|
+
@user_tags = @user_tags&.map do |t|
|
|
683
|
+
t.is_a?(InstagramUserTag) ? t : InstagramUserTag.new(**t.transform_keys(&:to_sym))
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# User tags must reach the wire as plain hashes, with unset coordinates
|
|
688
|
+
# dropped rather than sent as nulls.
|
|
689
|
+
def to_h
|
|
690
|
+
result = super
|
|
691
|
+
tags = result[:user_tags]
|
|
692
|
+
return result if tags.nil?
|
|
693
|
+
|
|
694
|
+
result[:user_tags] = tags.map do |t|
|
|
695
|
+
(t.is_a?(Model) ? t.to_h : t).reject { |_, v| v.nil? }
|
|
696
|
+
end
|
|
697
|
+
result
|
|
698
|
+
end
|
|
573
699
|
end
|
|
574
700
|
|
|
575
701
|
class TikTokParams < Model
|
|
@@ -596,7 +722,9 @@ module PostProxy
|
|
|
596
722
|
end
|
|
597
723
|
|
|
598
724
|
class TwitterParams < Model
|
|
599
|
-
|
|
725
|
+
# poll_options and poll_duration_minutes are required when format is
|
|
726
|
+
# "poll": 2-4 options (max 25 chars each), duration 5 to 10080 minutes.
|
|
727
|
+
attr_accessor :format, :poll_options, :poll_duration_minutes
|
|
600
728
|
end
|
|
601
729
|
|
|
602
730
|
class BlueskyParams < Model
|
data/lib/postproxy/version.rb
CHANGED