posthubify 0.1.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.
@@ -0,0 +1,882 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Posthubify
4
+ # Inbox — conversations + messages (Node sdk .inbox).
5
+ class InboxResource
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ # Conversation list (paginated envelope; req → returns data+pagination together).
11
+ def conversations(platform: nil, account_id: nil, limit: nil, cursor: nil, sort_order: nil)
12
+ @http.req('GET', '/inbox/conversations', query: {
13
+ 'platform' => platform, 'accountId' => account_id, 'limit' => limit,
14
+ 'cursor' => cursor, 'sortOrder' => sort_order
15
+ })
16
+ end
17
+
18
+ # Messages in a conversation.
19
+ def messages(conversation_id)
20
+ @http.data('GET', "/inbox/conversations/#{conversation_id}/messages")
21
+ end
22
+
23
+ # Send a message to a conversation.
24
+ def send_message(conversation_id, text)
25
+ @http.data('POST', "/inbox/conversations/#{conversation_id}/messages", body: { 'text' => text })
26
+ end
27
+
28
+ # Start a new conversation (recipientId = PSID/IGSID/convoId/handle).
29
+ def start_conversation(input)
30
+ @http.data('POST', '/inbox/conversations', body: input)
31
+ end
32
+ end
33
+
34
+ # Comment management — list/reply/hide/like/delete (Node sdk .comments).
35
+ class CommentsResource
36
+ def initialize(http)
37
+ @http = http
38
+ end
39
+
40
+ # Comment list (paginated envelope).
41
+ def list(platform: nil, account_id: nil, limit: nil, cursor: nil)
42
+ @http.req('GET', '/inbox/comments', query: {
43
+ 'platform' => platform, 'accountId' => account_id, 'limit' => limit, 'cursor' => cursor
44
+ })
45
+ end
46
+
47
+ # A post's comments.
48
+ def for_post(post_id, account_id)
49
+ @http.data('GET', "/inbox/posts/#{post_id}/comments", query: { 'accountId' => account_id })
50
+ end
51
+
52
+ # Reply to a comment (text or media; the user provides camelCase keys).
53
+ def reply(comment_id, input)
54
+ @http.data('POST', "/inbox/comments/#{comment_id}/reply", body: input)
55
+ end
56
+
57
+ # Private reply to a commenter (IG/FB) — opens a DM without requiring an open window.
58
+ def private_reply(comment_id, input)
59
+ @http.data('POST', "/inbox/comments/#{comment_id}/private-reply", body: input)
60
+ end
61
+
62
+ # Write a top-level comment on a post.
63
+ def create_on_post(post_id, input)
64
+ @http.data('POST', "/inbox/posts/#{post_id}/comments", body: input)
65
+ end
66
+
67
+ # Hide/show a comment.
68
+ def hide(comment_id, input)
69
+ @http.data('POST', "/inbox/comments/#{comment_id}/hide", body: input)
70
+ end
71
+
72
+ # Like a comment.
73
+ def like(comment_id, input)
74
+ @http.data('POST', "/inbox/comments/#{comment_id}/like", body: input)
75
+ end
76
+
77
+ # Delete a comment.
78
+ def delete(comment_id, account_id)
79
+ @http.data('DELETE', "/inbox/comments/#{comment_id}", query: { 'accountId' => account_id })
80
+ end
81
+ end
82
+
83
+ # Review management — list/reply/delete-reply (Node sdk .reviews).
84
+ class ReviewsResource
85
+ def initialize(http)
86
+ @http = http
87
+ end
88
+
89
+ # Review list (paginated envelope + summary).
90
+ def list(platform: nil, account_id: nil, min_rating: nil, max_rating: nil, limit: nil, cursor: nil)
91
+ @http.req('GET', '/reviews', query: {
92
+ 'platform' => platform, 'accountId' => account_id, 'minRating' => min_rating,
93
+ 'maxRating' => max_rating, 'limit' => limit, 'cursor' => cursor
94
+ })
95
+ end
96
+
97
+ # Delete a review reply.
98
+ def delete_reply(reply_id, account_id)
99
+ @http.data('DELETE', "/reviews/replies/#{reply_id}", query: { 'accountId' => account_id })
100
+ end
101
+
102
+ # Write a reply to a review.
103
+ def reply(context_id, input)
104
+ @http.data('POST', "/reviews/#{context_id}/reply", body: input)
105
+ end
106
+ end
107
+
108
+ # Contact management — CRUD + channels + bulk import (Node sdk .contacts).
109
+ class ContactsResource
110
+ def initialize(http)
111
+ @http = http
112
+ end
113
+
114
+ # Contact list (paginated envelope).
115
+ def list(search: nil, tag: nil, limit: nil, cursor: nil)
116
+ @http.req('GET', '/contacts', query: {
117
+ 'search' => search, 'tag' => tag, 'limit' => limit, 'cursor' => cursor
118
+ })
119
+ end
120
+
121
+ # A single contact.
122
+ def get(id)
123
+ @http.data('GET', "/contacts/#{id}")
124
+ end
125
+
126
+ # Channel-first idempotent upsert: an existing contact is returned inside the envelope with 'existing' => true →
127
+ # unwrap the data envelope and merge the existing flag (mirrors Node async create).
128
+ def create(input)
129
+ r = @http.req('POST', '/contacts', body: input)
130
+ contact = r.is_a?(Hash) && r.key?('data') ? r['data'] : r
131
+ if r.is_a?(Hash) && r['existing'] && contact.is_a?(Hash)
132
+ contact.merge('existing' => true)
133
+ else
134
+ contact
135
+ end
136
+ end
137
+
138
+ # Update a contact (partial).
139
+ def update(id, input)
140
+ @http.data('PATCH', "/contacts/#{id}", body: input)
141
+ end
142
+
143
+ # Delete a contact.
144
+ def delete(id)
145
+ @http.data('DELETE', "/contacts/#{id}")
146
+ end
147
+
148
+ # A contact's channels.
149
+ def channels(id)
150
+ @http.data('GET', "/contacts/#{id}/channels")
151
+ end
152
+
153
+ # Add a channel to a contact.
154
+ def add_channel(id, input)
155
+ @http.data('POST', "/contacts/#{id}/channels", body: input)
156
+ end
157
+
158
+ # Bulk import (1-1000); channel-first upsert avoids piling up duplicates.
159
+ def bulk(contacts)
160
+ @http.data('POST', '/contacts/bulk', body: { 'contacts' => contacts })
161
+ end
162
+ end
163
+
164
+ # Automation management — CRUD + activate/pause/duplicate + runs + dry-test (Node sdk .automations).
165
+ class AutomationsResource
166
+ def initialize(http)
167
+ @http = http
168
+ end
169
+
170
+ # Automation list.
171
+ def list
172
+ @http.data('GET', '/automations')
173
+ end
174
+
175
+ # A single automation.
176
+ def get(id)
177
+ @http.data('GET', "/automations/#{id}")
178
+ end
179
+
180
+ # Create an automation (trigger + action).
181
+ def create(input)
182
+ @http.data('POST', '/automations', body: input)
183
+ end
184
+
185
+ # Update an automation (partial).
186
+ def update(id, input)
187
+ @http.data('PATCH', "/automations/#{id}", body: input)
188
+ end
189
+
190
+ # Delete an automation.
191
+ def delete(id)
192
+ @http.data('DELETE', "/automations/#{id}")
193
+ end
194
+
195
+ # Activate an automation.
196
+ def activate(id)
197
+ @http.data('POST', "/automations/#{id}/activate")
198
+ end
199
+
200
+ # Pause an automation.
201
+ def pause(id)
202
+ @http.data('POST', "/automations/#{id}/pause")
203
+ end
204
+
205
+ # The copy starts PAUSED (protection against accidental double-triggering).
206
+ def duplicate(id)
207
+ @http.data('POST', "/automations/#{id}/duplicate")
208
+ end
209
+
210
+ # Automation run records.
211
+ def runs(id, limit: nil)
212
+ @http.data('GET', "/automations/#{id}/runs", query: { 'limit' => limit })
213
+ end
214
+
215
+ # Dry-run: does a synthetic event match the trigger (nothing is sent).
216
+ def test(id, input = {})
217
+ @http.data('POST', "/automations/#{id}/test", body: input)
218
+ end
219
+ end
220
+
221
+ # PHASE D4 — IG/FB comment→DM automations (per-post/story, buttons, click-tracking).
222
+ class CommentAutomationsResource
223
+ def initialize(http)
224
+ @http = http
225
+ end
226
+
227
+ # List comment-automations (filter: account_id/platform/enabled).
228
+ def list(account_id: nil, platform: nil, enabled: nil)
229
+ @http.data('GET', '/comment-automations',
230
+ query: { 'accountId' => account_id, 'platform' => platform, 'enabled' => enabled })
231
+ end
232
+
233
+ # A single comment-automation (including funnel stats).
234
+ def get(id)
235
+ @http.data('GET', "/comment-automations/#{id}")
236
+ end
237
+
238
+ # Create a comment→DM automation. input: {name, platform, accountId, triggerType?, targeting?, ...}.
239
+ def create(input)
240
+ @http.data('POST', '/comment-automations', body: input)
241
+ end
242
+
243
+ # Update a comment-automation (partial; platform/account cannot change).
244
+ def update(id, input)
245
+ @http.data('PATCH', "/comment-automations/#{id}", body: input)
246
+ end
247
+
248
+ # Delete a comment-automation.
249
+ def delete(id)
250
+ @http.data('DELETE', "/comment-automations/#{id}")
251
+ end
252
+
253
+ # Per-comment trigger audit ledger (paginated).
254
+ def logs(id, limit: nil, cursor: nil)
255
+ @http.req('GET', "/comment-automations/#{id}/logs", query: { 'limit' => limit, 'cursor' => cursor })
256
+ end
257
+ end
258
+
259
+ # PHASE D5 — graph-based conversation automation (node/edge, versioning, run timeline).
260
+ class WorkflowsResource
261
+ def initialize(http)
262
+ @http = http
263
+ end
264
+
265
+ def list(account_id: nil, status: nil)
266
+ @http.data('GET', '/workflows', query: { 'accountId' => account_id, 'status' => status })
267
+ end
268
+
269
+ def get(id)
270
+ @http.data('GET', "/workflows/#{id}")
271
+ end
272
+
273
+ # Draft workflow from a node/edge graph. input: {name, accountId, nodes?, edges?, triggerConfig?}.
274
+ def create(input)
275
+ @http.data('POST', '/workflows', body: input)
276
+ end
277
+
278
+ # F3 — generate a graph with AI from a natural-language command. create:true → a draft is saved (returns the workflow).
279
+ def generate(command, account_id, create: false)
280
+ @http.data('POST', '/workflows/generate', body: { 'command' => command, 'accountId' => account_id, 'create' => create })
281
+ end
282
+
283
+ def update(id, input)
284
+ @http.data('PATCH', "/workflows/#{id}", body: input)
285
+ end
286
+
287
+ def delete(id)
288
+ @http.data('DELETE', "/workflows/#{id}")
289
+ end
290
+
291
+ def activate(id)
292
+ @http.data('POST', "/workflows/#{id}/activate")
293
+ end
294
+
295
+ def pause(id)
296
+ @http.data('POST', "/workflows/#{id}/pause")
297
+ end
298
+
299
+ def duplicate(id)
300
+ @http.data('POST', "/workflows/#{id}/duplicate")
301
+ end
302
+
303
+ def executions(id, limit: nil, cursor: nil)
304
+ @http.req('GET', "/workflows/#{id}/executions", query: { 'limit' => limit, 'cursor' => cursor })
305
+ end
306
+
307
+ # Manual test-run (side-effect-free dry-run). input: {message?, variables?}. → {execution, events}.
308
+ def run_test(id, input = {})
309
+ @http.data('POST', "/workflows/#{id}/executions", body: input)
310
+ end
311
+
312
+ def events(id, execution_id)
313
+ @http.data('GET', "/workflows/#{id}/executions/#{execution_id}/events")
314
+ end
315
+
316
+ def versions(id)
317
+ @http.data('GET', "/workflows/#{id}/versions")
318
+ end
319
+
320
+ def version(id, version)
321
+ @http.data('GET', "/workflows/#{id}/versions/#{version}")
322
+ end
323
+
324
+ # Restore the live graph to a snapshot (reversible).
325
+ def restore_version(id, version)
326
+ @http.data('POST', "/workflows/#{id}/versions/#{version}/restore")
327
+ end
328
+
329
+ # Store a BYOK provider key for the ai node (write-only, encrypted).
330
+ # provider ∈ anthropic|openai|google|mistral|groq.
331
+ def set_secret(id, provider, api_key)
332
+ @http.data('PUT', "/workflows/#{id}/secrets/#{provider}", body: { 'apiKey' => api_key })
333
+ end
334
+ end
335
+
336
+ # Broadcast — CRUD + send/schedule/cancel + recipients (Node sdk .broadcasts).
337
+ class BroadcastsResource
338
+ def initialize(http)
339
+ @http = http
340
+ end
341
+
342
+ # Broadcast list.
343
+ def list
344
+ @http.data('GET', '/broadcasts')
345
+ end
346
+
347
+ # A single broadcast.
348
+ def get(id)
349
+ @http.data('GET', "/broadcasts/#{id}")
350
+ end
351
+
352
+ # Create a broadcast (safe retry with idempotency_key).
353
+ def create(input, idempotency_key: nil)
354
+ @http.data('POST', '/broadcasts', body: input, idempotency_key: idempotency_key)
355
+ end
356
+
357
+ # Editable only in the draft/scheduled/failed stage.
358
+ def update(id, input)
359
+ @http.data('PATCH', "/broadcasts/#{id}", body: input)
360
+ end
361
+
362
+ # Delete a broadcast.
363
+ def delete(id)
364
+ @http.data('DELETE', "/broadcasts/#{id}")
365
+ end
366
+
367
+ # Asynchronous (202): sending happens in the background; poll progress with get(id).
368
+ def send(id)
369
+ @http.data('POST', "/broadcasts/#{id}/send")
370
+ end
371
+
372
+ # draft → scheduled; when the time comes the server scheduler starts the send.
373
+ def schedule(id, scheduled_at)
374
+ @http.data('POST', "/broadcasts/#{id}/schedule", body: { 'scheduledAt' => scheduled_at })
375
+ end
376
+
377
+ # scheduled → draft (content is preserved).
378
+ def cancel(id)
379
+ @http.data('POST', "/broadcasts/#{id}/cancel")
380
+ end
381
+
382
+ # Recipients: contact name + per-target status (paginated envelope).
383
+ def recipients(id, limit: nil, cursor: nil)
384
+ @http.req('GET', "/broadcasts/#{id}/recipients", query: { 'limit' => limit, 'cursor' => cursor })
385
+ end
386
+
387
+ # Add recipients (duplicates are skipped) — only draft/scheduled.
388
+ def add_recipients(id, contact_ids)
389
+ @http.data('POST', "/broadcasts/#{id}/recipients", body: { 'contactIds' => contact_ids })
390
+ end
391
+ end
392
+
393
+ # Message sequence — CRUD + activate/pause + enroll/unenroll (Node sdk .sequences).
394
+ class SequencesResource
395
+ def initialize(http)
396
+ @http = http
397
+ end
398
+
399
+ # Sequence list (paginated envelope).
400
+ def list(status: nil, limit: nil, cursor: nil)
401
+ @http.req('GET', '/sequences', query: { 'status' => status, 'limit' => limit, 'cursor' => cursor })
402
+ end
403
+
404
+ # A single sequence.
405
+ def get(id)
406
+ @http.data('GET', "/sequences/#{id}")
407
+ end
408
+
409
+ # Create a sequence (steps + status/platform/account_id/exit_on_reply/exit_on_unsubscribe).
410
+ def create(input)
411
+ @http.data('POST', '/sequences', body: input)
412
+ end
413
+
414
+ # Update a sequence (partial).
415
+ def update(id, input)
416
+ @http.data('PATCH', "/sequences/#{id}", body: input)
417
+ end
418
+
419
+ # Delete a sequence.
420
+ def delete(id)
421
+ @http.data('DELETE', "/sequences/#{id}")
422
+ end
423
+
424
+ # Activate a sequence.
425
+ def activate(id)
426
+ @http.data('POST', "/sequences/#{id}/activate")
427
+ end
428
+
429
+ # Pause a sequence.
430
+ def pause(id)
431
+ @http.data('POST', "/sequences/#{id}/pause")
432
+ end
433
+
434
+ # Enroll contacts into a sequence.
435
+ def enroll(id, contact_ids)
436
+ @http.data('POST', "/sequences/#{id}/enroll", body: { 'contactIds' => contact_ids })
437
+ end
438
+
439
+ # Sequence enrollments.
440
+ def enrollments(id)
441
+ @http.data('GET', "/sequences/#{id}/enrollments")
442
+ end
443
+
444
+ # Remove a contact from a sequence.
445
+ def unenroll(id, contact_id)
446
+ @http.data('POST', "/sequences/#{id}/unenroll", body: { 'contactId' => contact_id })
447
+ end
448
+ end
449
+ # Custom fields (D3) — definition CRUD + per-contact value (Node sdk .customFields).
450
+ class CustomFieldsResource
451
+ def initialize(http)
452
+ @http = http
453
+ end
454
+
455
+ def list
456
+ @http.data('GET', '/custom-fields')
457
+ end
458
+
459
+ def create(input)
460
+ @http.data('POST', '/custom-fields', body: input)
461
+ end
462
+
463
+ def update(key, input)
464
+ @http.data('PATCH', "/custom-fields/#{key}", body: input)
465
+ end
466
+
467
+ def delete(key)
468
+ @http.data('DELETE', "/custom-fields/#{key}")
469
+ end
470
+
471
+ def values_for(contact_id)
472
+ @http.data('GET', "/contacts/#{contact_id}/fields")
473
+ end
474
+
475
+ def set_value(contact_id, key, value)
476
+ @http.data('PUT', "/contacts/#{contact_id}/fields/#{key}", body: { 'value' => value })
477
+ end
478
+
479
+ def clear_value(contact_id, key)
480
+ @http.data('DELETE', "/contacts/#{contact_id}/fields/#{key}")
481
+ end
482
+ end
483
+
484
+ # WhatsApp Business — message templates (D1). account_id = the connected WhatsApp account.
485
+ # Only status=APPROVED templates can be sent; creation goes to Meta for approval (PENDING).
486
+ class WhatsAppResource
487
+ def initialize(http)
488
+ @http = http
489
+ end
490
+
491
+ def list_templates(account_id, status: nil, limit: nil)
492
+ @http.data('GET', "/whatsapp/#{account_id}/templates",
493
+ query: { 'status' => status, 'limit' => limit })
494
+ end
495
+
496
+ def get_template(account_id, name)
497
+ @http.data('GET', "/whatsapp/#{account_id}/templates/#{name}")
498
+ end
499
+
500
+ def create_template(account_id, input)
501
+ @http.data('POST', "/whatsapp/#{account_id}/templates", body: input)
502
+ end
503
+
504
+ def edit_template(account_id, template_id, input)
505
+ @http.data('PATCH', "/whatsapp/#{account_id}/templates/#{template_id}", body: input)
506
+ end
507
+
508
+ def delete_template(account_id, name, hsm_id: nil)
509
+ @http.data('DELETE', "/whatsapp/#{account_id}/templates/#{name}", query: { 'hsmId' => hsm_id })
510
+ end
511
+
512
+ def send_template(account_id, input)
513
+ @http.data('POST', "/whatsapp/#{account_id}/template-messages", body: input)
514
+ end
515
+
516
+ def get_profile(account_id)
517
+ @http.data('GET', "/whatsapp/#{account_id}/profile")
518
+ end
519
+
520
+ def update_profile(account_id, input)
521
+ @http.data('PATCH', "/whatsapp/#{account_id}/profile", body: input)
522
+ end
523
+
524
+ def request_code(account_id, input)
525
+ @http.data('POST', "/whatsapp/#{account_id}/request-code", body: input)
526
+ end
527
+
528
+ def verify_code(account_id, code)
529
+ @http.data('POST', "/whatsapp/#{account_id}/verify-code", body: { 'code' => code })
530
+ end
531
+
532
+ def register(account_id, pin)
533
+ @http.data('POST', "/whatsapp/#{account_id}/register", body: { 'pin' => pin })
534
+ end
535
+
536
+ def set_two_step_pin(account_id, pin)
537
+ @http.data('POST', "/whatsapp/#{account_id}/two-step", body: { 'pin' => pin })
538
+ end
539
+
540
+ def list_blocked(account_id, limit: nil)
541
+ @http.data('GET', "/whatsapp/#{account_id}/blocked", query: { 'limit' => limit })
542
+ end
543
+
544
+ def block(account_id, users)
545
+ @http.data('POST', "/whatsapp/#{account_id}/block", body: { 'users' => users })
546
+ end
547
+
548
+ def unblock(account_id, users)
549
+ @http.data('POST', "/whatsapp/#{account_id}/unblock", body: { 'users' => users })
550
+ end
551
+
552
+ # --- Flows (D1e) ---
553
+ def list_flows(account_id)
554
+ @http.data('GET', "/whatsapp/#{account_id}/flows")
555
+ end
556
+
557
+ def get_flow(account_id, flow_id)
558
+ @http.data('GET', "/whatsapp/#{account_id}/flows/#{flow_id}")
559
+ end
560
+
561
+ def create_flow(account_id, input)
562
+ @http.data('POST', "/whatsapp/#{account_id}/flows", body: input)
563
+ end
564
+
565
+ def update_flow(account_id, flow_id, input)
566
+ @http.data('PATCH', "/whatsapp/#{account_id}/flows/#{flow_id}", body: input)
567
+ end
568
+
569
+ def delete_flow(account_id, flow_id)
570
+ @http.data('DELETE', "/whatsapp/#{account_id}/flows/#{flow_id}")
571
+ end
572
+
573
+ def upload_flow_json(account_id, flow_id, flow_json)
574
+ @http.data('PUT', "/whatsapp/#{account_id}/flows/#{flow_id}/json", body: { 'flowJson' => flow_json })
575
+ end
576
+
577
+ def publish_flow(account_id, flow_id)
578
+ @http.data('POST', "/whatsapp/#{account_id}/flows/#{flow_id}/publish")
579
+ end
580
+
581
+ def deprecate_flow(account_id, flow_id)
582
+ @http.data('POST', "/whatsapp/#{account_id}/flows/#{flow_id}/deprecate")
583
+ end
584
+
585
+ def get_flow_preview(account_id, flow_id, invalidate: nil)
586
+ @http.data('GET', "/whatsapp/#{account_id}/flows/#{flow_id}/preview",
587
+ query: { 'invalidate' => invalidate })
588
+ end
589
+
590
+ # --- Sandbox (D1f) — shared test number sessions ---
591
+ def sandbox_list_sessions
592
+ @http.data('GET', '/whatsapp/sandbox/sessions')
593
+ end
594
+
595
+ def sandbox_create_session(phone)
596
+ @http.data('POST', '/whatsapp/sandbox/sessions', body: { 'phone' => phone })
597
+ end
598
+
599
+ def sandbox_revoke_session(session_id)
600
+ @http.data('DELETE', "/whatsapp/sandbox/sessions/#{session_id}")
601
+ end
602
+
603
+ # --- Calling (D1g) — voice calls ---
604
+ def calling_get_config(account_id)
605
+ @http.data('GET', '/whatsapp/calling', query: { 'accountId' => account_id })
606
+ end
607
+
608
+ def calling_enable(account_id, input)
609
+ @http.data('POST', "/whatsapp/phone-numbers/#{account_id}/calling", body: input)
610
+ end
611
+
612
+ def calling_update(account_id, input)
613
+ @http.data('PATCH', "/whatsapp/phone-numbers/#{account_id}/calling", body: input)
614
+ end
615
+
616
+ def calling_disable(account_id)
617
+ @http.data('DELETE', "/whatsapp/phone-numbers/#{account_id}/calling")
618
+ end
619
+
620
+ def call_permissions(account_id, to)
621
+ @http.data('GET', '/whatsapp/call-permissions', query: { 'accountId' => account_id, 'to' => to })
622
+ end
623
+
624
+ def place_call(account_id, input)
625
+ @http.data('POST', '/whatsapp/calls', body: { 'accountId' => account_id }.merge(input))
626
+ end
627
+
628
+ def list_calls(account_id, limit: nil)
629
+ @http.data('GET', '/whatsapp/calls', query: { 'accountId' => account_id, 'limit' => limit })
630
+ end
631
+
632
+ def get_call(account_id, call_id)
633
+ @http.data('GET', "/whatsapp/calls/#{call_id}", query: { 'accountId' => account_id })
634
+ end
635
+
636
+ def estimate_call(to, recording: nil)
637
+ @http.data('GET', '/whatsapp/calls/estimate', query: { 'to' => to, 'recording' => recording })
638
+ end
639
+ end
640
+
641
+ # Google Business Profile (D2) — location + review. account_id = the connected GBP account.
642
+ class GmbResource
643
+ def initialize(http)
644
+ @http = http
645
+ end
646
+
647
+ def list_locations(account_id, page_token: nil)
648
+ @http.data('GET', "/accounts/#{account_id}/gmb-locations", query: { 'pageToken' => page_token })
649
+ end
650
+
651
+ def switch_location(account_id, location_name)
652
+ @http.data('PUT', "/accounts/#{account_id}/gmb-locations", body: { 'locationName' => location_name })
653
+ end
654
+
655
+ def list_reviews(account_id, location_id: nil, page_token: nil)
656
+ @http.data('GET', "/accounts/#{account_id}/gmb-reviews",
657
+ query: { 'locationId' => location_id, 'pageToken' => page_token })
658
+ end
659
+
660
+ def batch_reviews(account_id, location_names)
661
+ @http.data('POST', "/accounts/#{account_id}/gmb-reviews/batch",
662
+ body: { 'locationNames' => location_names })
663
+ end
664
+
665
+ def reply_review(account_id, review_id, comment, location_id: nil)
666
+ @http.data('POST', "/accounts/#{account_id}/gmb-reviews/#{review_id}/reply",
667
+ body: { 'comment' => comment }, query: { 'locationId' => location_id })
668
+ end
669
+
670
+ def delete_review_reply(account_id, review_id, location_id: nil)
671
+ @http.data('DELETE', "/accounts/#{account_id}/gmb-reviews/#{review_id}/reply",
672
+ query: { 'locationId' => location_id })
673
+ end
674
+
675
+ # ── Listing content (D2 cp3) ──
676
+ def get_location_details(account_id, location_id: nil, read_mask: nil)
677
+ @http.data('GET', "/accounts/#{account_id}/gmb-location-details",
678
+ query: { 'locationId' => location_id, 'readMask' => read_mask })
679
+ end
680
+
681
+ def update_location_details(account_id, update_mask, patch, location_id: nil)
682
+ @http.data('PUT', "/accounts/#{account_id}/gmb-location-details",
683
+ body: { 'updateMask' => update_mask, 'patch' => patch },
684
+ query: { 'locationId' => location_id })
685
+ end
686
+
687
+ def get_attributes(account_id, location_id: nil)
688
+ @http.data('GET', "/accounts/#{account_id}/gmb-attributes", query: { 'locationId' => location_id })
689
+ end
690
+
691
+ def update_attributes(account_id, attributes, attribute_mask, location_id: nil)
692
+ @http.data('PUT', "/accounts/#{account_id}/gmb-attributes",
693
+ body: { 'attributes' => attributes, 'attributeMask' => attribute_mask },
694
+ query: { 'locationId' => location_id })
695
+ end
696
+
697
+ def list_attribute_metadata(account_id, language_code:, location_id: nil, category_name: nil,
698
+ region_code: nil, page_size: nil, page_token: nil)
699
+ @http.data('GET', "/accounts/#{account_id}/gmb-attribute-metadata",
700
+ query: { 'languageCode' => language_code, 'locationId' => location_id,
701
+ 'categoryName' => category_name, 'regionCode' => region_code,
702
+ 'pageSize' => page_size, 'pageToken' => page_token })
703
+ end
704
+
705
+ def get_services(account_id, location_id: nil)
706
+ @http.data('GET', "/accounts/#{account_id}/gmb-services", query: { 'locationId' => location_id })
707
+ end
708
+
709
+ def update_services(account_id, service_items, location_id: nil)
710
+ @http.data('PUT', "/accounts/#{account_id}/gmb-services",
711
+ body: { 'serviceItems' => service_items }, query: { 'locationId' => location_id })
712
+ end
713
+
714
+ def get_food_menus(account_id, location_id: nil)
715
+ @http.data('GET', "/accounts/#{account_id}/gmb-food-menus", query: { 'locationId' => location_id })
716
+ end
717
+
718
+ def update_food_menus(account_id, menus, update_mask: nil, location_id: nil)
719
+ @http.data('PUT', "/accounts/#{account_id}/gmb-food-menus",
720
+ body: { 'menus' => menus, 'updateMask' => update_mask },
721
+ query: { 'locationId' => location_id })
722
+ end
723
+
724
+ # ── Media (D2 cp3) ──
725
+ def list_media(account_id, location_id: nil, page_size: nil, page_token: nil)
726
+ @http.data('GET', "/accounts/#{account_id}/gmb-media",
727
+ query: { 'locationId' => location_id, 'pageSize' => page_size, 'pageToken' => page_token })
728
+ end
729
+
730
+ def create_media(account_id, source_url, category, location_id: nil)
731
+ @http.data('POST', "/accounts/#{account_id}/gmb-media",
732
+ body: { 'sourceUrl' => source_url, 'category' => category },
733
+ query: { 'locationId' => location_id })
734
+ end
735
+
736
+ def delete_media(account_id, media_id, location_id: nil)
737
+ @http.data('DELETE', "/accounts/#{account_id}/gmb-media",
738
+ query: { 'mediaId' => media_id, 'locationId' => location_id })
739
+ end
740
+
741
+ # ── Place actions (D2 cp3) ──
742
+ def list_place_actions(account_id, location_id: nil, page_size: nil, page_token: nil)
743
+ @http.data('GET', "/accounts/#{account_id}/gmb-place-actions",
744
+ query: { 'locationId' => location_id, 'pageSize' => page_size, 'pageToken' => page_token })
745
+ end
746
+
747
+ def create_place_action(account_id, uri, place_action_type, location_id: nil)
748
+ @http.data('POST', "/accounts/#{account_id}/gmb-place-actions",
749
+ body: { 'uri' => uri, 'placeActionType' => place_action_type },
750
+ query: { 'locationId' => location_id })
751
+ end
752
+
753
+ def update_place_action(account_id, name, uri: nil, place_action_type: nil, location_id: nil)
754
+ @http.data('PATCH', "/accounts/#{account_id}/gmb-place-actions",
755
+ body: { 'name' => name, 'uri' => uri, 'placeActionType' => place_action_type },
756
+ query: { 'locationId' => location_id })
757
+ end
758
+
759
+ def delete_place_action(account_id, name, location_id: nil)
760
+ @http.data('DELETE', "/accounts/#{account_id}/gmb-place-actions",
761
+ query: { 'name' => name, 'locationId' => location_id })
762
+ end
763
+
764
+ # ── Verification (D2 cp3) ──
765
+ def get_verifications(account_id, location_id: nil)
766
+ @http.data('GET', "/accounts/#{account_id}/gmb-verifications", query: { 'locationId' => location_id })
767
+ end
768
+
769
+ def start_verification(account_id, body, location_id: nil)
770
+ @http.data('POST', "/accounts/#{account_id}/gmb-verifications",
771
+ body: body, query: { 'locationId' => location_id })
772
+ end
773
+
774
+ def fetch_verification_options(account_id, language_code, context: nil, location_id: nil)
775
+ @http.data('POST', "/accounts/#{account_id}/gmb-verifications/options",
776
+ body: { 'languageCode' => language_code, 'context' => context },
777
+ query: { 'locationId' => location_id })
778
+ end
779
+
780
+ def complete_verification(account_id, verification_id, pin, location_id: nil)
781
+ @http.data('POST', "/accounts/#{account_id}/gmb-verifications/#{verification_id}/complete",
782
+ body: { 'pin' => pin }, query: { 'locationId' => location_id })
783
+ end
784
+ end
785
+
786
+ # Discord community-ops (D6) — role/member/event/pin/DM/settings. account_id scopes to the connected account.
787
+ class DiscordResource
788
+ def initialize(http)
789
+ @http = http
790
+ end
791
+
792
+ def list_roles(account_id, guild_id)
793
+ @http.data('GET', "/discord/guilds/#{guild_id}/roles", query: { 'accountId' => account_id })
794
+ end
795
+
796
+ def list_members(account_id, guild_id, limit: nil, after: nil)
797
+ @http.data('GET', "/discord/guilds/#{guild_id}/members",
798
+ query: { 'accountId' => account_id, 'limit' => limit, 'after' => after })
799
+ end
800
+
801
+ def assign_role(account_id, guild_id, user_id, role_id)
802
+ @http.data('PUT', "/discord/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}",
803
+ query: { 'accountId' => account_id })
804
+ end
805
+
806
+ def remove_role(account_id, guild_id, user_id, role_id)
807
+ @http.data('DELETE', "/discord/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}",
808
+ query: { 'accountId' => account_id })
809
+ end
810
+
811
+ def list_events(account_id, guild_id, with_user_count: nil)
812
+ @http.data('GET', "/discord/guilds/#{guild_id}/events",
813
+ query: { 'accountId' => account_id, 'withUserCount' => with_user_count })
814
+ end
815
+
816
+ def create_event(account_id, guild_id, body)
817
+ @http.data('POST', "/discord/guilds/#{guild_id}/events", query: { 'accountId' => account_id }, body: body)
818
+ end
819
+
820
+ def get_event(account_id, guild_id, event_id, with_user_count: nil)
821
+ @http.data('GET', "/discord/guilds/#{guild_id}/events/#{event_id}",
822
+ query: { 'accountId' => account_id, 'withUserCount' => with_user_count })
823
+ end
824
+
825
+ def update_event(account_id, guild_id, event_id, body)
826
+ @http.data('PATCH', "/discord/guilds/#{guild_id}/events/#{event_id}",
827
+ query: { 'accountId' => account_id }, body: body)
828
+ end
829
+
830
+ def delete_event(account_id, guild_id, event_id)
831
+ @http.data('DELETE', "/discord/guilds/#{guild_id}/events/#{event_id}", query: { 'accountId' => account_id })
832
+ end
833
+
834
+ def list_pins(account_id, channel_id)
835
+ @http.data('GET', "/discord/channels/#{channel_id}/pins", query: { 'accountId' => account_id })
836
+ end
837
+
838
+ def pin(account_id, channel_id, message_id)
839
+ @http.data('PUT', "/discord/channels/#{channel_id}/pins/#{message_id}", query: { 'accountId' => account_id })
840
+ end
841
+
842
+ def unpin(account_id, channel_id, message_id)
843
+ @http.data('DELETE', "/discord/channels/#{channel_id}/pins/#{message_id}", query: { 'accountId' => account_id })
844
+ end
845
+
846
+ def send_dm(body)
847
+ @http.data('POST', '/discord/dms', body: body)
848
+ end
849
+
850
+ def list_channels(account_id)
851
+ @http.data('GET', "/accounts/#{account_id}/discord-channels")
852
+ end
853
+
854
+ def get_settings(account_id)
855
+ @http.data('GET', "/accounts/#{account_id}/discord-settings")
856
+ end
857
+
858
+ def update_settings(account_id, patch)
859
+ @http.data('PATCH', "/accounts/#{account_id}/discord-settings", body: patch)
860
+ end
861
+ end
862
+
863
+ # Content discovery (D7) — read-only Reddit feed/search + LinkedIn mention via the connected account.
864
+ class DiscoveryResource
865
+ def initialize(http)
866
+ @http = http
867
+ end
868
+
869
+ def reddit_feed(account_id, subreddit, query = {})
870
+ @http.data('GET', '/reddit/feed', query: { 'accountId' => account_id, 'subreddit' => subreddit }.merge(query))
871
+ end
872
+
873
+ def reddit_search(account_id, q, query = {})
874
+ @http.data('GET', '/reddit/search', query: { 'accountId' => account_id, 'q' => q }.merge(query))
875
+ end
876
+
877
+ def linkedin_mentions(account_id, q)
878
+ @http.data('GET', "/accounts/#{account_id}/linkedin-mentions", query: { 'q' => q })
879
+ end
880
+ end
881
+
882
+ end