postproxy-sdk 1.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fabdc6a06ca3cef409cf88533c925ff183e3e1c7089fd53aa541c09bff0aa6f
4
- data.tar.gz: 7b4245a53e37aab93f006d408430676dbe3352a8815e5479de765946d941e20e
3
+ metadata.gz: a6be6a31464b3afaf473473d94fd639b2553782cf0494e87ccfebb3e5e2e5e4a
4
+ data.tar.gz: 0ff052e8410c536e2938cf6887cecee713f615afa11207c88f733c29b6770d07
5
5
  SHA512:
6
- metadata.gz: 97e32211fa2f54a2a28b506333108a484153083efcf9272bcf23fc821e27e6e2e88be7dd7d166acf6f9cd339a69a07afd1fb9b9f3c271a860b8ec9f5b41b7a9e
7
- data.tar.gz: 3217e3f75dd540a6c13215dc5a5b7830d483ebd38f4cd30433162e7c732f69b214b06e3f73ccfbe5683f953a9c57133bf9dd1de4bdaf4ffe5dc3a631c15a132e
6
+ metadata.gz: 2a67b96326d47a4c33463c10b5346ff28debf6e4faf7ef11a297fded79abf9cac4b062099cc1e1cf6002e6e9f2bc4b9e641dbf26537552f8d2041310598a8163
7
+ data.tar.gz: cf8c37298d421c37886440e75819a9a1462d34ccadad79e4623cbb2cb431292e4ead2e1ba0014bd13b522c9c0e62737eeb524987bde5a97f99404e3416686950
data/README.md CHANGED
@@ -464,6 +464,73 @@ message = client.comments.private_reply("post-id", "comment-id", profile_id: "pr
464
464
  puts message.chat_id, message.status
465
465
  ```
466
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
+
467
534
  ## Profile comments (Google Business reviews)
468
535
 
469
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.
@@ -22,9 +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, profile_group_id: nil,
27
- idempotency_key: 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)
28
35
  has_files = media_files && !media_files.empty?
29
36
 
30
37
  if has_files
@@ -58,6 +65,9 @@ module PostProxy
58
65
  json_body[:tag] = tag if tag
59
66
  json_body[:reply_to_external_id] = reply_to_external_id if reply_to_external_id
60
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
61
71
 
62
72
  result = @client.request(:post, "/chats/#{chat_id}/messages",
63
73
  json: json_body,
@@ -111,6 +121,13 @@ module PostProxy
111
121
 
112
122
  private
113
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
+
114
131
  def mime_type_for(filename)
115
132
  case File.extname(filename).downcase
116
133
  when ".jpg", ".jpeg" then "image/jpeg"
@@ -429,11 +429,94 @@ module PostProxy
429
429
  end
430
430
  end
431
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
+
432
514
  class Message < Model
433
515
  attr_accessor :id, :chat_id, :external_id, :direction, :body, :status,
434
516
  :tag, :external_comment_id, :error_message, :platform_data,
435
517
  :external_posted_at, :external_delivered_at, :external_read_at,
436
518
  :external_edited_at, :reply_to_external_id, :reply_markup,
519
+ :quick_replies, :buttons, :card, :tapped_action,
437
520
  :external_deleted_at, :reactions, :attachments,
438
521
  :is_unsupported, :created_at
439
522
 
@@ -450,6 +533,10 @@ module PostProxy
450
533
  @external_edited_at = nil
451
534
  @reply_to_external_id = nil
452
535
  @reply_markup = nil
536
+ @quick_replies = nil
537
+ @buttons = nil
538
+ @card = nil
539
+ @tapped_action = nil
453
540
  @external_deleted_at = nil
454
541
  @reactions = []
455
542
  @attachments = []
@@ -467,6 +554,20 @@ module PostProxy
467
554
  @attachments = (@attachments || []).map do |a|
468
555
  a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
469
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
470
571
  end
471
572
 
472
573
  private
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.12.0"
2
+ VERSION = "1.13.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postproxy-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PostProxy