ruby-whatsapp 0.4.0 → 0.6.1

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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/README.md +322 -617
  4. data/docs/README.md +76 -0
  5. data/docs/business_phone_number/README.md +243 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/contact_book.md +90 -0
  8. data/docs/business_phone_number/profile.md +220 -0
  9. data/docs/business_phone_number/username.md +157 -0
  10. data/docs/configuration.md +177 -0
  11. data/docs/errors.md +219 -0
  12. data/docs/media/README.md +161 -0
  13. data/docs/message_templates/README.md +206 -0
  14. data/docs/message_templates/authentication.md +107 -0
  15. data/docs/message_templates/carousel.md +103 -0
  16. data/docs/message_templates/components.md +215 -0
  17. data/docs/message_templates/library.md +98 -0
  18. data/docs/message_templates/limited_time_offer.md +94 -0
  19. data/docs/message_templates/responses.md +173 -0
  20. data/docs/message_templates/standard.md +138 -0
  21. data/docs/messages/README.md +212 -0
  22. data/docs/messages/address.md +86 -0
  23. data/docs/messages/audio.md +43 -0
  24. data/docs/messages/contacts.md +129 -0
  25. data/docs/messages/document.md +49 -0
  26. data/docs/messages/image.md +59 -0
  27. data/docs/messages/interactive.md +342 -0
  28. data/docs/messages/location.md +51 -0
  29. data/docs/messages/location_request.md +59 -0
  30. data/docs/messages/mark_message_as_read.md +84 -0
  31. data/docs/messages/reaction.md +59 -0
  32. data/docs/messages/sticker.md +40 -0
  33. data/docs/messages/template.md +193 -0
  34. data/docs/messages/text.md +55 -0
  35. data/docs/messages/video.md +41 -0
  36. data/docs/schedule/README.md +183 -0
  37. data/docs/subscribed_app/README.md +139 -0
  38. data/docs/webhooks/README.md +217 -0
  39. data/docs/webhooks/account_alerts.md +44 -0
  40. data/docs/webhooks/account_review_update.md +29 -0
  41. data/docs/webhooks/account_update.md +54 -0
  42. data/docs/webhooks/automatic_events.md +47 -0
  43. data/docs/webhooks/business_capability_update.md +40 -0
  44. data/docs/webhooks/business_username_updates.md +55 -0
  45. data/docs/webhooks/history.md +107 -0
  46. data/docs/webhooks/message_template_components_update.md +46 -0
  47. data/docs/webhooks/message_template_quality_update.md +49 -0
  48. data/docs/webhooks/message_template_status_update.md +55 -0
  49. data/docs/webhooks/messages.md +520 -0
  50. data/docs/webhooks/partner_solutions.md +35 -0
  51. data/docs/webhooks/payment_configuration_update.md +40 -0
  52. data/docs/webhooks/phone_number_name_update.md +44 -0
  53. data/docs/webhooks/phone_number_quality_update.md +40 -0
  54. data/docs/webhooks/security.md +43 -0
  55. data/docs/webhooks/smb_app_state_sync.md +91 -0
  56. data/docs/webhooks/smb_message_echoes.md +91 -0
  57. data/docs/webhooks/template_category_update.md +52 -0
  58. data/docs/webhooks/user_id_update.md +84 -0
  59. data/docs/webhooks/user_preferences.md +87 -0
  60. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  61. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  62. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  63. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  64. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  65. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  66. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  67. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  68. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  69. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  70. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  71. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  72. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  73. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  74. data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
  75. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  76. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  77. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  78. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  79. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  80. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  81. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  82. data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
  83. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  84. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  85. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  86. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  87. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  88. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  89. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  90. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  91. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  92. data/lib/ruby/whatsapp/path_building.rb +33 -0
  93. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  94. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  95. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  96. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  97. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  98. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  99. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  100. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  101. data/lib/ruby/whatsapp/schedule.rb +15 -0
  102. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  103. data/lib/ruby/whatsapp/version.rb +1 -1
  104. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  105. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  106. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  107. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  108. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  109. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  110. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  111. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  112. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  113. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  114. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  115. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  116. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  117. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  118. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  119. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  120. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  121. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  122. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  123. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  124. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  125. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  126. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  127. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  128. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  129. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  130. metadata +100 -1
@@ -2,50 +2,89 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # Chat history synchronization for onboarded business customers.
5
+ # Chat history synchronization for onboarded business customers — one of the fields
6
+ # with a confirmed, JSON-example-backed schema, alongside {Messages},
7
+ # {UserIdUpdate}, {BusinessUsernameUpdates}, and {UserPreferences}.
6
8
  #
7
- # Best-effort schema: Meta's public docs describe this field in one line with
8
- # no published JSON example; `threads`' entry shape isn't confidently
9
- # typeable from that description alone, so it's kept as a raw array rather
10
- # than guessing further. Validate against a real payload before relying on
11
- # this in production.
12
- # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
+ # History arrives in chunks. Each `history[]` entry is one {Chunk}, carrying its own
10
+ # sync `metadata` (`phase`/`chunk_order`/`progress`) and the {Thread}s in it — note
11
+ # that the value-level `metadata` is the ordinary business-phone-number
12
+ # {Whatsapp::Webhook::Metadata}, a different object entirely.
13
+ #
14
+ # Meta sends a second variant of this field for a synced media asset, which carries
15
+ # `contacts` and `messages` at the value level instead of inside a thread; both are
16
+ # mapped here, and are empty for a thread-carrying payload.
17
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
13
18
  class History
14
- # @!attribute [rw] phase
15
- # @return [Integer, nil]
16
- attr_accessor :phase
17
-
18
- # @!attribute [rw] chunk_order
19
- # @return [Integer, nil]
20
- attr_accessor :chunk_order
21
-
22
- # @!attribute [rw] progress
23
- # @return [Integer, nil]
24
- attr_accessor :progress
25
-
26
- # @!attribute [rw] threads
27
- # @return [Array<Hash>]
28
- attr_accessor :threads
29
-
30
- def initialize(phase:, chunk_order:, progress:, threads:)
31
- @phase = phase
32
- @chunk_order = chunk_order
33
- @progress = progress
34
- @threads = threads
19
+ # @!attribute [rw] messaging_product
20
+ # @return [String, nil]
21
+ attr_accessor :messaging_product
22
+
23
+ # @!attribute [rw] metadata
24
+ # @return [Metadata, nil] The business phone number this history belongs to — not
25
+ # the per-chunk sync metadata, which lives on {Chunk}.
26
+ attr_accessor :metadata
27
+
28
+ # @!attribute [rw] chunks
29
+ # @return [Array<Chunk>]
30
+ attr_accessor :chunks
31
+
32
+ # @!attribute [rw] contacts
33
+ # @return [Array<Contact>] Populated only by the media-asset variant.
34
+ attr_accessor :contacts
35
+
36
+ # @!attribute [rw] messages
37
+ # @return [Array<Message::Base>] Populated only by the media-asset variant; a
38
+ # thread-carrying payload puts its messages on {Thread#messages} instead.
39
+ attr_accessor :messages
40
+
41
+ # @!attribute [r] raw
42
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part of
43
+ # the supported API: this field has two payload variants and the deeper one nests
44
+ # three levels, so a key that drifts would leave a typed accessor empty with no
45
+ # other trace. Treat it as read-only — it is the object `JSON.parse` returned.
46
+ attr_reader :raw
47
+
48
+ def initialize(messaging_product:, metadata:, chunks:, contacts:, messages:, raw: {})
49
+ @messaging_product = messaging_product
50
+ @metadata = metadata
51
+ @chunks = chunks
52
+ @contacts = contacts
53
+ @messages = messages
54
+ @raw = raw
55
+ end
56
+
57
+ # A delivery normally carries exactly one chunk, so these three read through to it
58
+ # rather than making every caller reach for `chunks.first`. Check {#chunks} directly
59
+ # if you need to handle a multi-chunk delivery.
60
+ # @return [Integer, nil]
61
+ def phase
62
+ chunks.first&.phase
63
+ end
64
+
65
+ # @return [Integer, nil]
66
+ def chunk_order
67
+ chunks.first&.chunk_order
68
+ end
69
+
70
+ # @return [Integer, nil] Percent complete, 0-100.
71
+ def progress
72
+ chunks.first&.progress
35
73
  end
36
74
 
37
75
  class << self
38
- # @param data [Hash] The raw `value` hash.
76
+ # @param data [Hash] The raw `history` field's `value` hash.
39
77
  # @return [History]
40
78
  def deserialize(data)
41
79
  data ||= {}
42
- metadata = data["metadata"] || {}
43
80
 
44
81
  new(
45
- phase: metadata["phase"],
46
- chunk_order: metadata["chunk_order"],
47
- progress: metadata["progress"],
48
- threads: Array(data["threads"])
82
+ messaging_product: data["messaging_product"],
83
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
84
+ chunks: Array(data["history"]).map { |chunk_data| Chunk.deserialize(chunk_data) },
85
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
86
+ messages: Array(data["messages"]).map { |message_data| Message.deserialize(message_data) },
87
+ raw: data
49
88
  )
50
89
  end
51
90
  end
@@ -7,9 +7,24 @@ module Whatsapp
7
7
  # wamid, when it arrived, and (optionally) what it's replying to.
8
8
  class Base
9
9
  # @!attribute [rw] from
10
- # @return [String, nil]
10
+ # @return [String, nil] Omitted once the sender adopts a username, unless a
11
+ # recent interaction or the contact book still allows sharing the phone number.
11
12
  attr_accessor :from
12
13
 
14
+ # @!attribute [rw] from_user_id
15
+ # @return [String, nil] The sender's business-scoped user ID (BSUID).
16
+ attr_accessor :from_user_id
17
+
18
+ # @!attribute [rw] from_parent_user_id
19
+ # @return [String, nil] The sender's parent BSUID, present only when enrolled.
20
+ attr_accessor :from_parent_user_id
21
+
22
+ # @!attribute [rw] group_id
23
+ # @return [String, nil] Set only when the message was sent in a group chat.
24
+ # Reading groups works; this gem wraps no Groups API, so there is no way to
25
+ # reply into one — the id is for correlation and for filtering group traffic out.
26
+ attr_accessor :group_id
27
+
13
28
  # @!attribute [rw] id
14
29
  # @return [String, nil] The message's wamid.
15
30
  attr_accessor :id
@@ -30,8 +45,15 @@ module Whatsapp
30
45
  # @return [Referral, nil]
31
46
  attr_accessor :referral
32
47
 
33
- def initialize(from:, id:, timestamp:, type:, context: nil, referral: nil)
48
+ def initialize(
49
+ id:, timestamp:, type:,
50
+ from: nil, from_user_id: nil, from_parent_user_id: nil, group_id: nil,
51
+ context: nil, referral: nil
52
+ )
34
53
  @from = from
54
+ @from_user_id = from_user_id
55
+ @from_parent_user_id = from_parent_user_id
56
+ @group_id = group_id
35
57
  @id = id
36
58
  @timestamp = timestamp
37
59
  @type = type
@@ -47,6 +69,9 @@ module Whatsapp
47
69
  def common_attributes(data)
48
70
  {
49
71
  from: data["from"],
72
+ from_user_id: data["from_user_id"],
73
+ from_parent_user_id: data["from_parent_user_id"],
74
+ group_id: data["group_id"],
50
75
  id: data["id"],
51
76
  timestamp: data["timestamp"],
52
77
  type: data["type"],
@@ -34,7 +34,16 @@ module Whatsapp
34
34
  # @return [String, nil] Date in YYYY-MM-DD format.
35
35
  attr_accessor :birthday
36
36
 
37
- def initialize(name:, phones:, emails:, addresses:, urls:, org: nil, birthday: nil)
37
+ # @!attribute [rw] vcard
38
+ # @return [String, nil] The card's raw vCard, set only when `origin` is `"other"`.
39
+ attr_accessor :vcard
40
+
41
+ # @!attribute [rw] origin
42
+ # @return [String, nil] `"contact_request"` (a `REQUEST_CONTACT_INFO` button tap)
43
+ # or `"other"` (shared directly from the chat).
44
+ attr_accessor :origin
45
+
46
+ def initialize(name:, phones:, emails:, addresses:, urls:, org: nil, birthday: nil, vcard: nil, origin: nil)
38
47
  @name = name
39
48
  @phones = phones
40
49
  @emails = emails
@@ -42,6 +51,8 @@ module Whatsapp
42
51
  @org = org
43
52
  @urls = urls
44
53
  @birthday = birthday
54
+ @vcard = vcard
55
+ @origin = origin
45
56
  end
46
57
 
47
58
  class << self
@@ -57,7 +68,9 @@ module Whatsapp
57
68
  addresses: Array(data["addresses"]).map { |address_data| Address.deserialize(address_data) },
58
69
  org: data["org"] ? Org.deserialize(data["org"]) : nil,
59
70
  urls: Array(data["urls"]).map { |url_data| Url.deserialize(url_data) },
60
- birthday: data["birthday"]
71
+ birthday: data["birthday"],
72
+ vcard: data["vcard"],
73
+ origin: data["origin"]
61
74
  )
62
75
  end
63
76
  end
@@ -7,9 +7,22 @@ module Whatsapp
7
7
  # swipe, or a tap on an interactive button/list from an earlier message).
8
8
  class Context
9
9
  # @!attribute [rw] from
10
- # @return [String, nil]
10
+ # @return [String, nil] The WhatsApp ID of the quoted message's sender, absent
11
+ # once they adopt a username and their phone number is no longer shareable.
11
12
  attr_accessor :from
12
13
 
14
+ # @!attribute [rw] from_user_id
15
+ # @return [String, nil] The quoted sender's BSUID, the fallback identifier once
16
+ # {#from} is gone. **The key name is inferred** by symmetry with
17
+ # {Base#from_user_id} — Meta has not published a `context` example carrying
18
+ # it. Validate against a real payload before depending on it.
19
+ attr_accessor :from_user_id
20
+
21
+ # @!attribute [rw] from_parent_user_id
22
+ # @return [String, nil] The quoted sender's parent BSUID, present only for
23
+ # businesses enrolled in parent BSUIDs. Key name inferred, as above.
24
+ attr_accessor :from_parent_user_id
25
+
13
26
  # @!attribute [rw] id
14
27
  # @return [String, nil] The wamid of the message being replied to.
15
28
  attr_accessor :id
@@ -22,8 +35,14 @@ module Whatsapp
22
35
  # @return [Boolean, nil]
23
36
  attr_accessor :frequently_forwarded
24
37
 
25
- def initialize(from:, id:, forwarded: nil, frequently_forwarded: nil)
38
+ def initialize(
39
+ id:,
40
+ from: nil, from_user_id: nil, from_parent_user_id: nil,
41
+ forwarded: nil, frequently_forwarded: nil
42
+ )
26
43
  @from = from
44
+ @from_user_id = from_user_id
45
+ @from_parent_user_id = from_parent_user_id
27
46
  @id = id
28
47
  @forwarded = forwarded
29
48
  @frequently_forwarded = frequently_forwarded
@@ -37,6 +56,8 @@ module Whatsapp
37
56
 
38
57
  new(
39
58
  from: data["from"],
59
+ from_user_id: data["from_user_id"],
60
+ from_parent_user_id: data["from_parent_user_id"],
40
61
  id: data["id"],
41
62
  forwarded: data["forwarded"],
42
63
  frequently_forwarded: data["frequently_forwarded"]
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Message
6
+ # A user edited a message they had sent you. Like {Revoke}, a control message: it
7
+ # names the message being replaced and carries the replacement content.
8
+ #
9
+ # Also arrives through {Whatsapp::Webhook::SmbMessageEchoes} when the business
10
+ # itself edits a message from the WhatsApp Business app.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/edit
12
+ class Edit < Base
13
+ # @!attribute [rw] original_message_id
14
+ # @return [String, nil] The wamid of the message being replaced.
15
+ attr_accessor :original_message_id
16
+
17
+ # @!attribute [rw] message
18
+ # @return [Base, nil] The replacement content, dispatched by its own `type`
19
+ # through {Message.deserialize} like any other message.
20
+ #
21
+ # It carries *content only*. Meta's example gives the nested payload a `type`,
22
+ # its type body, and a `context`, but no `from`/`id`/`timestamp` of its own —
23
+ # those belong to the outer envelope, so the nested message's equivalents read
24
+ # `nil`. Reach for `edit.id` and `edit.from`, not `edit.message.id`.
25
+ attr_accessor :message
26
+
27
+ def initialize(original_message_id: nil, message: nil, **base_attributes)
28
+ super(**base_attributes)
29
+
30
+ @original_message_id = original_message_id
31
+ @message = message
32
+ end
33
+
34
+ class << self
35
+ # @param data [Hash] The raw message hash.
36
+ # @return [Edit]
37
+ def deserialize(data)
38
+ edit = data["edit"] || {}
39
+
40
+ new(
41
+ original_message_id: edit["original_message_id"],
42
+ message: edit["message"] ? Message.deserialize(edit["message"]) : nil,
43
+ **common_attributes(data)
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Message
6
+ # A user deleted a message they had sent you. A control message rather than a
7
+ # content one: it names a message you already received instead of carrying a body.
8
+ #
9
+ # Also arrives through {Whatsapp::Webhook::SmbMessageEchoes} when the business
10
+ # itself revokes a message from the WhatsApp Business app.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/revoke
12
+ class Revoke < Base
13
+ # @!attribute [rw] original_message_id
14
+ # @return [String, nil] The wamid of the message that was deleted.
15
+ attr_accessor :original_message_id
16
+
17
+ def initialize(original_message_id: nil, **base_attributes)
18
+ super(**base_attributes)
19
+
20
+ @original_message_id = original_message_id
21
+ end
22
+
23
+ class << self
24
+ # @param data [Hash] The raw message hash.
25
+ # @return [Revoke]
26
+ def deserialize(data)
27
+ new(
28
+ original_message_id: data.dig("revoke", "original_message_id"),
29
+ **common_attributes(data)
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -17,16 +17,31 @@ module Whatsapp
17
17
  # @return [String, nil] The customer's (possibly new) WhatsApp ID.
18
18
  attr_accessor :wa_id
19
19
 
20
+ # @!attribute [rw] user_id
21
+ # @return [String, nil] The customer's new business-scoped user ID (BSUID),
22
+ # set when the change was a `user_changed_user_id` event.
23
+ attr_accessor :user_id
24
+
25
+ # @!attribute [rw] parent_user_id
26
+ # @return [String, nil] The customer's new parent BSUID, present only when enrolled.
27
+ attr_accessor :parent_user_id
28
+
20
29
  # @!attribute [rw] change_type
21
- # @return [String, nil] e.g. `"customer_changed_number"`, `"customer_identity_changed"`.
30
+ # @return [String, nil] e.g. `"customer_changed_number"`, `"user_changed_user_id"`.
22
31
  attr_accessor :change_type
23
32
 
24
- def initialize(body:, identity:, wa_id:, change_type:, **base_attributes)
33
+ def initialize(
34
+ change_type:,
35
+ body: nil, identity: nil, wa_id: nil, user_id: nil, parent_user_id: nil,
36
+ **base_attributes
37
+ )
25
38
  super(**base_attributes)
26
39
 
27
40
  @body = body
28
41
  @identity = identity
29
42
  @wa_id = wa_id
43
+ @user_id = user_id
44
+ @parent_user_id = parent_user_id
30
45
  @change_type = change_type
31
46
  end
32
47
 
@@ -38,6 +53,8 @@ module Whatsapp
38
53
  body: data.dig("system", "body"),
39
54
  identity: data.dig("system", "identity"),
40
55
  wa_id: data.dig("system", "wa_id"),
56
+ user_id: data.dig("system", "user_id"),
57
+ parent_user_id: data.dig("system", "parent_user_id"),
41
58
  change_type: data.dig("system", "type"),
42
59
  **common_attributes(data)
43
60
  )
@@ -7,9 +7,9 @@ module Whatsapp
7
7
  # type Meta added after this gem was released, or a message the Cloud API
8
8
  # itself couldn't process (carries an `errors[]` array in that case).
9
9
  class Unknown < Base
10
- # @!attribute [rw] raw
10
+ # @!attribute [r] raw
11
11
  # @return [Hash] The full, untyped raw message hash.
12
- attr_accessor :raw
12
+ attr_reader :raw
13
13
 
14
14
  # @!attribute [rw] errors
15
15
  # @return [Array<Error>]
@@ -22,6 +22,9 @@ module Whatsapp
22
22
  order: Order,
23
23
  system: System,
24
24
  reaction: Reaction,
25
+ # Control types: these name a prior message rather than carrying content of their own.
26
+ revoke: Revoke,
27
+ edit: Edit,
25
28
  }.freeze
26
29
 
27
30
  class << self
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # The `messages` webhook field's `value` payload — the only field with a
6
- # confirmed, JSON-example-backed schema in Meta's docs. Carries inbound
7
- # messages and/or delivery status updates for one of your business phone numbers.
5
+ # The `messages` webhook field's `value` payload — one of five fields with a
6
+ # confirmed, JSON-example-backed schema in Meta's docs, alongside {UserIdUpdate}
7
+ # and {BusinessUsernameUpdates}. Carries inbound messages and/or delivery status
8
+ # updates for one of your business phone numbers.
8
9
  # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
10
  class Messages
10
11
  # @!attribute [rw] messaging_product
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class SmbAppStateSync
6
+ # A synced address-book entry from the WhatsApp Business app.
7
+ #
8
+ # The gem's third `Contact`, and none of them are interchangeable:
9
+ # {Whatsapp::Webhook::Contact} is a notification's sender identity (`profile.name`,
10
+ # `wa_id`), {Whatsapp::Webhook::Message::Contacts::Contact} is a shared contact
11
+ # *card* of vCard-shaped arrays, and this is a device address-book row
12
+ # (`full_name`, `first_name`, `phone_number`). Forcing any two together would mean
13
+ # dual-key reads on both sides for no gain.
14
+ #
15
+ # A sibling of {StateSync} rather than nested inside it, following
16
+ # {UserIdUpdate::IdChange}.
17
+ class Contact
18
+ # @!attribute [rw] full_name
19
+ # @return [String, nil]
20
+ attr_accessor :full_name
21
+
22
+ # @!attribute [rw] first_name
23
+ # @return [String, nil]
24
+ attr_accessor :first_name
25
+
26
+ # @!attribute [rw] phone_number
27
+ # @return [String, nil] Omitted once the contact adopts a username and their
28
+ # number is no longer shareable. Fall back to {#user_id}.
29
+ attr_accessor :phone_number
30
+
31
+ # @!attribute [rw] user_id
32
+ # @return [String, nil] The contact's business-scoped user ID (BSUID).
33
+ attr_accessor :user_id
34
+
35
+ # @!attribute [rw] parent_user_id
36
+ # @return [String, nil] The contact's parent BSUID, present only when enrolled.
37
+ attr_accessor :parent_user_id
38
+
39
+ # @!attribute [rw] username
40
+ # @return [String, nil] Set only if the contact has adopted a username. Read
41
+ # flat, unlike {Whatsapp::Webhook::Contact#username}, which Meta nests under
42
+ # `profile` — do not copy that dual read here.
43
+ attr_accessor :username
44
+
45
+ def initialize(
46
+ full_name: nil, first_name: nil, phone_number: nil,
47
+ user_id: nil, parent_user_id: nil, username: nil
48
+ )
49
+ @full_name = full_name
50
+ @first_name = first_name
51
+ @phone_number = phone_number
52
+ @user_id = user_id
53
+ @parent_user_id = parent_user_id
54
+ @username = username
55
+ end
56
+
57
+ class << self
58
+ # @param data [Hash, nil] A raw `contact` hash.
59
+ # @return [Contact]
60
+ def deserialize(data)
61
+ data ||= {}
62
+
63
+ new(
64
+ full_name: data["full_name"],
65
+ first_name: data["first_name"],
66
+ phone_number: data["phone_number"],
67
+ user_id: data["user_id"],
68
+ parent_user_id: data["parent_user_id"],
69
+ username: data["username"]
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -6,7 +6,7 @@ module Whatsapp
6
6
  # A single contact-sync entry within an `smb_app_state_sync` notification.
7
7
  class StateSync
8
8
  # @!attribute [rw] type
9
- # @return [String, nil]
9
+ # @return [String, nil] `"contact"` is the only kind Meta documents.
10
10
  attr_accessor :type
11
11
 
12
12
  # @!attribute [rw] action
@@ -14,13 +14,22 @@ module Whatsapp
14
14
  attr_accessor :action
15
15
 
16
16
  # @!attribute [rw] contact
17
- # @return [Hash] Left untyped — Meta's docs don't publish this entry's schema.
17
+ # @return [Contact, nil] The synced address-book entry.
18
18
  attr_accessor :contact
19
19
 
20
- def initialize(type:, action:, contact:)
20
+ # @!attribute [rw] timestamp
21
+ # @return [String, nil] Flattened up from this entry's own `metadata.timestamp`.
22
+ # That nested object is *not* {Whatsapp::Webhook::Metadata} — it carries only a
23
+ # timestamp, so the wrapper is dropped rather than typed, the same habit as
24
+ # {Contact#profile_name}. Should Meta add a sibling key there, this becomes a
25
+ # class instead.
26
+ attr_accessor :timestamp
27
+
28
+ def initialize(type:, action:, contact: nil, timestamp: nil)
21
29
  @type = type
22
30
  @action = action
23
31
  @contact = contact
32
+ @timestamp = timestamp
24
33
  end
25
34
 
26
35
  class << self
@@ -29,7 +38,12 @@ module Whatsapp
29
38
  def deserialize(data)
30
39
  data ||= {}
31
40
 
32
- new(type: data["type"], action: data["action"], contact: data["contact"] || {})
41
+ new(
42
+ type: data["type"],
43
+ action: data["action"],
44
+ contact: data["contact"] ? Contact.deserialize(data["contact"]) : nil,
45
+ timestamp: data.dig("metadata", "timestamp")
46
+ )
33
47
  end
34
48
  end
35
49
  end
@@ -2,21 +2,40 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # Contact synchronization for onboarded WhatsApp Business app users.
5
+ # Contact synchronization for onboarded WhatsApp Business app users: the address book
6
+ # on the device, mirrored to your integration as entries are added, changed, or removed.
6
7
  #
7
- # Best-effort schema: Meta's public docs describe this field in one line with
8
- # no published JSON example; each entry's `contact` shape isn't confidently
9
- # typeable from that description alone, so it's kept as a raw hash rather
10
- # than guessing further. Validate against a real payload before relying on
11
- # this in production.
12
- # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
8
+ # Meta's BSUID documentation publishes the synced contact's shape, so
9
+ # {StateSync#contact} is a typed {Contact} rather than a raw hash. The array key
10
+ # itself (`state_sync`) is still inferred from the field name — hence {#raw}.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
13
12
  class SmbAppStateSync
13
+ # @!attribute [rw] messaging_product
14
+ # @return [String, nil]
15
+ attr_accessor :messaging_product
16
+
17
+ # @!attribute [rw] metadata
18
+ # @return [Metadata, nil] The business phone number whose address book this is —
19
+ # not the per-entry metadata, whose timestamp is flattened onto {StateSync}.
20
+ attr_accessor :metadata
21
+
14
22
  # @!attribute [rw] state_sync
15
23
  # @return [Array<StateSync>]
16
24
  attr_accessor :state_sync
17
25
 
18
- def initialize(state_sync:)
26
+ # @!attribute [r] raw
27
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part of
28
+ # the supported API: {#state_sync} reads `value["state_sync"]`, a key inferred
29
+ # from the field's own name rather than confirmed by a published example, so a
30
+ # key that drifts would leave the array empty with no other trace. Read-only —
31
+ # it is the object `JSON.parse` returned, not a copy.
32
+ attr_reader :raw
33
+
34
+ def initialize(state_sync:, messaging_product: nil, metadata: nil, raw: {})
35
+ @messaging_product = messaging_product
36
+ @metadata = metadata
19
37
  @state_sync = state_sync
38
+ @raw = raw
20
39
  end
21
40
 
22
41
  class << self
@@ -25,7 +44,12 @@ module Whatsapp
25
44
  def deserialize(data)
26
45
  data ||= {}
27
46
 
28
- new(state_sync: Array(data["state_sync"]).map { |entry| StateSync.deserialize(entry) })
47
+ new(
48
+ messaging_product: data["messaging_product"],
49
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
50
+ state_sync: Array(data["state_sync"]).map { |entry| StateSync.deserialize(entry) },
51
+ raw: data
52
+ )
29
53
  end
30
54
  end
31
55
  end