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
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class SmbMessageEchoes
6
+ # One echoed message: who the business sent it *to*, plus the message itself.
7
+ #
8
+ # A wrapper rather than three more accessors on {Message::Base}, deliberately.
9
+ # `Base` is the envelope shared by every inbound message type, and an inbound
10
+ # message can never carry a recipient — Meta's own convention is that a keyword
11
+ # defaults to `nil` when a field is *conditional*, not when it is impossible.
12
+ # Putting `to`/`to_user_id`/`to_parent_user_id` there to serve this one field would
13
+ # hand fifteen classes three permanently-`nil` readers, and would turn the
14
+ # `NoMethodError` that currently tells you `message.to` is meaningless into a silent
15
+ # `nil`. Composing instead keeps {Message.deserialize}'s dispatch untouched, so an
16
+ # echoed `revoke` or `edit` resolves exactly as an inbound one does.
17
+ class Echo
18
+ # @!attribute [rw] to
19
+ # @return [String, nil] The customer's phone number. Omitted once they adopt a
20
+ # username and it is no longer shareable — fall back to {#to_user_id}.
21
+ attr_accessor :to
22
+
23
+ # @!attribute [rw] to_user_id
24
+ # @return [String, nil] The customer's business-scoped user ID (BSUID).
25
+ attr_accessor :to_user_id
26
+
27
+ # @!attribute [rw] to_parent_user_id
28
+ # @return [String, nil] The customer's parent BSUID, present only when enrolled.
29
+ attr_accessor :to_parent_user_id
30
+
31
+ # @!attribute [rw] message
32
+ # @return [Message::Base] The echoed message, dispatched by `type` like any
33
+ # inbound one. Its `from` is the *business* number, not the customer's.
34
+ attr_accessor :message
35
+
36
+ def initialize(message:, to: nil, to_user_id: nil, to_parent_user_id: nil)
37
+ @to = to
38
+ @to_user_id = to_user_id
39
+ @to_parent_user_id = to_parent_user_id
40
+ @message = message
41
+ end
42
+
43
+ class << self
44
+ # @param data [Hash, nil] A raw `message_echoes[]` entry.
45
+ # @return [Echo]
46
+ def deserialize(data)
47
+ data ||= {}
48
+
49
+ new(
50
+ to: data["to"],
51
+ to_user_id: data["to_user_id"],
52
+ to_parent_user_id: data["to_parent_user_id"],
53
+ message: Message.deserialize(data)
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,15 +2,14 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # Messages sent via the WhatsApp Business App or a linked device, echoed back
6
- # so a Cloud API integration stays in sync with what the business itself sent.
5
+ # Messages sent via the WhatsApp Business App or a linked device, echoed back so a
6
+ # Cloud API integration stays in sync with what the business itself sent — including
7
+ # messages the business revoked or edited from the app.
7
8
  #
8
- # Best-effort schema: Meta's public docs describe this field in one line with
9
- # no published JSON example, but do state these mirror the standard inbound
10
- # `messages` shape — hence reusing {Message.deserialize} here. The exact key
11
- # name (`message_echoes`) is a best-effort guess; validate against a real
12
- # payload before relying on it in production.
13
- # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
+ # Meta's BSUID documentation publishes the value-level `contacts` array and the
10
+ # per-echo recipient identifiers. The array key itself (`message_echoes`) remains
11
+ # inferred, hence {#raw}.
12
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
14
13
  class SmbMessageEchoes
15
14
  # @!attribute [rw] messaging_product
16
15
  # @return [String, nil]
@@ -20,14 +19,28 @@ module Whatsapp
20
19
  # @return [Metadata, nil]
21
20
  attr_accessor :metadata
22
21
 
22
+ # @!attribute [rw] contacts
23
+ # @return [Array<Contact>] The customers these messages went to.
24
+ attr_accessor :contacts
25
+
23
26
  # @!attribute [rw] message_echoes
24
- # @return [Array<Message::Base>]
27
+ # @return [Array<Echo>] Each pairs the echoed message with who it was sent to.
25
28
  attr_accessor :message_echoes
26
29
 
27
- def initialize(messaging_product:, metadata:, message_echoes:)
30
+ # @!attribute [r] raw
31
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part of
32
+ # the supported API: {#message_echoes} reads `value["message_echoes"]`, a key
33
+ # inferred from the field's own name rather than confirmed by a published
34
+ # example, so a key that drifts would leave the array empty with no other trace.
35
+ # Read-only — it is the object `JSON.parse` returned, not a copy.
36
+ attr_reader :raw
37
+
38
+ def initialize(messaging_product:, metadata:, message_echoes:, contacts: [], raw: {})
28
39
  @messaging_product = messaging_product
29
40
  @metadata = metadata
41
+ @contacts = contacts
30
42
  @message_echoes = message_echoes
43
+ @raw = raw
31
44
  end
32
45
 
33
46
  class << self
@@ -39,7 +52,9 @@ module Whatsapp
39
52
  new(
40
53
  messaging_product: data["messaging_product"],
41
54
  metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
42
- message_echoes: Array(data["message_echoes"]).map { |echo_data| Message.deserialize(echo_data) }
55
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
56
+ message_echoes: Array(data["message_echoes"]).map { |echo_data| Echo.deserialize(echo_data) },
57
+ raw: data
43
58
  )
44
59
  end
45
60
  end
@@ -18,9 +18,22 @@ module Whatsapp
18
18
  attr_accessor :timestamp
19
19
 
20
20
  # @!attribute [rw] recipient_id
21
- # @return [String, nil]
21
+ # @return [String, nil] Omitted when sent to the recipient's BSUID and their
22
+ # phone number is unavailable.
22
23
  attr_accessor :recipient_id
23
24
 
25
+ # @!attribute [rw] recipient_user_id
26
+ # @return [String, nil] The recipient's business-scoped user ID (BSUID). Meta sends
27
+ # it on every status, however the message was addressed — a phone number in `to`
28
+ # does not suppress it. The single exception is a `"failed"` status for a message
29
+ # sent to a phone number, where it is omitted; a `"failed"` status for a
30
+ # BSUID-addressed message still carries it.
31
+ attr_accessor :recipient_user_id
32
+
33
+ # @!attribute [rw] recipient_parent_user_id
34
+ # @return [String, nil] The recipient's parent BSUID, present only when enrolled.
35
+ attr_accessor :recipient_parent_user_id
36
+
24
37
  # @!attribute [rw] conversation
25
38
  # @return [Conversation, nil]
26
39
  attr_accessor :conversation
@@ -33,11 +46,17 @@ module Whatsapp
33
46
  # @return [Array<Error>]
34
47
  attr_accessor :errors
35
48
 
36
- def initialize(id:, status:, timestamp:, recipient_id:, conversation: nil, pricing: nil, errors: [])
49
+ def initialize(
50
+ id:, status:, timestamp:,
51
+ recipient_id: nil, recipient_user_id: nil, recipient_parent_user_id: nil,
52
+ conversation: nil, pricing: nil, errors: []
53
+ )
37
54
  @id = id
38
55
  @status = status
39
56
  @timestamp = timestamp
40
57
  @recipient_id = recipient_id
58
+ @recipient_user_id = recipient_user_id
59
+ @recipient_parent_user_id = recipient_parent_user_id
41
60
  @conversation = conversation
42
61
  @pricing = pricing
43
62
  @errors = errors
@@ -54,6 +73,8 @@ module Whatsapp
54
73
  status: data["status"],
55
74
  timestamp: data["timestamp"],
56
75
  recipient_id: data["recipient_id"],
76
+ recipient_user_id: data["recipient_user_id"],
77
+ recipient_parent_user_id: data["recipient_parent_user_id"],
57
78
  conversation: data["conversation"] ? Conversation.deserialize(data["conversation"]) : nil,
58
79
  pricing: data["pricing"] ? Pricing.deserialize(data["pricing"]) : nil,
59
80
  errors: Array(data["errors"]).map { |error_data| Error.deserialize(error_data) }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class UserIdUpdate
6
+ # A single `user_id_update[]` entry: one user's BSUID (and parent BSUID, if
7
+ # enrolled) changed.
8
+ class Entry
9
+ # @!attribute [rw] wa_id
10
+ # @return [String, nil] The user's phone number, if available.
11
+ attr_accessor :wa_id
12
+
13
+ # @!attribute [rw] detail
14
+ # @return [String, nil] Human-readable description of the change.
15
+ attr_accessor :detail
16
+
17
+ # @!attribute [rw] user_id
18
+ # @return [IdChange, nil] The user's previous and current BSUID.
19
+ attr_accessor :user_id
20
+
21
+ # @!attribute [rw] parent_user_id
22
+ # @return [IdChange, nil] The user's previous and current parent BSUID,
23
+ # present only when enrolled.
24
+ attr_accessor :parent_user_id
25
+
26
+ # @!attribute [rw] timestamp
27
+ # @return [String, nil]
28
+ attr_accessor :timestamp
29
+
30
+ def initialize(timestamp:, wa_id: nil, detail: nil, user_id: nil, parent_user_id: nil)
31
+ @wa_id = wa_id
32
+ @detail = detail
33
+ @user_id = user_id
34
+ @parent_user_id = parent_user_id
35
+ @timestamp = timestamp
36
+ end
37
+
38
+ class << self
39
+ # @param data [Hash] A raw `user_id_update[]` entry.
40
+ # @return [Entry]
41
+ def deserialize(data)
42
+ data ||= {}
43
+
44
+ new(
45
+ wa_id: data["wa_id"],
46
+ detail: data["detail"],
47
+ user_id: IdChange.deserialize(data["user_id"]),
48
+ parent_user_id: IdChange.deserialize(data["parent_user_id"]),
49
+ timestamp: data["timestamp"]
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class UserIdUpdate
6
+ # The `{previous, current}` shape shared by `user_id` and `parent_user_id` on a
7
+ # `user_id_update[]` entry.
8
+ class IdChange
9
+ # @!attribute [rw] previous
10
+ # @return [String, nil]
11
+ attr_accessor :previous
12
+
13
+ # @!attribute [rw] current
14
+ # @return [String, nil]
15
+ attr_accessor :current
16
+
17
+ def initialize(previous:, current:)
18
+ @previous = previous
19
+ @current = current
20
+ end
21
+
22
+ class << self
23
+ # @param data [Hash, nil] The raw `user_id` or `parent_user_id` hash.
24
+ # @return [IdChange, nil] `nil` when the key is absent — an id change that did
25
+ # not happen is not a change from nothing to nothing.
26
+ def deserialize(data)
27
+ return if data.nil?
28
+
29
+ new(previous: data["previous"], current: data["current"])
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # A WhatsApp user's business-scoped user ID (BSUID) changed — for example, after
6
+ # they changed their phone number (see the `system` message type's
7
+ # `user_changed_user_id` event) or were re-scoped by Meta.
8
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
9
+ class UserIdUpdate
10
+ # @!attribute [rw] messaging_product
11
+ # @return [String, nil]
12
+ attr_accessor :messaging_product
13
+
14
+ # @!attribute [rw] metadata
15
+ # @return [Metadata, nil]
16
+ attr_accessor :metadata
17
+
18
+ # @!attribute [rw] contacts
19
+ # @return [Array<Contact>]
20
+ attr_accessor :contacts
21
+
22
+ # @!attribute [rw] updates
23
+ # @return [Array<Entry>]
24
+ attr_accessor :updates
25
+
26
+ # @!attribute [r] raw
27
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part
28
+ # of the supported API: {#updates} reads `value["user_id_update"]` — entries
29
+ # nested under a repeat of the field's own name, which is what Meta's published
30
+ # example shows — so a key that drifts would otherwise leave `updates` empty
31
+ # with no other trace. Reach for it when a typed accessor is unexpectedly
32
+ # `nil`, log what you find, and treat it as read-only: it is the object
33
+ # `JSON.parse` returned, not a copy.
34
+ attr_reader :raw
35
+
36
+ def initialize(messaging_product:, metadata:, contacts:, updates:, raw: {})
37
+ @messaging_product = messaging_product
38
+ @metadata = metadata
39
+ @contacts = contacts
40
+ @updates = updates
41
+ @raw = raw
42
+ end
43
+
44
+ class << self
45
+ # @param data [Hash] The raw `user_id_update` field's `value` hash.
46
+ # @return [UserIdUpdate]
47
+ def deserialize(data)
48
+ data ||= {}
49
+
50
+ new(
51
+ messaging_product: data["messaging_product"],
52
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
53
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
54
+ updates: Array(data["user_id_update"]).map { |entry_data| Entry.deserialize(entry_data) },
55
+ raw: data
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class UserPreferences
6
+ # A single `user_preferences[]` entry: one user changed one preference.
7
+ class Entry
8
+ # @!attribute [rw] wa_id
9
+ # @return [String, nil] The user's phone number, omitted once they adopt a
10
+ # username and it is no longer shareable. Fall back to {#user_id}.
11
+ attr_accessor :wa_id
12
+
13
+ # @!attribute [rw] user_id
14
+ # @return [String, nil] The user's business-scoped user ID (BSUID).
15
+ attr_accessor :user_id
16
+
17
+ # @!attribute [rw] parent_user_id
18
+ # @return [String, nil] The user's parent BSUID, present only when enrolled.
19
+ attr_accessor :parent_user_id
20
+
21
+ # @!attribute [rw] detail
22
+ # @return [String, nil] Human-readable description of the change.
23
+ attr_accessor :detail
24
+
25
+ # @!attribute [rw] category
26
+ # @return [String, nil] The preference category. `"marketing_messages"` is the
27
+ # only value Meta documents; kept a raw String, never validated on the way in,
28
+ # so a category added later still round-trips.
29
+ attr_accessor :category
30
+
31
+ # @!attribute [rw] value
32
+ # @return [String, nil] The preference itself, e.g. `"stop"` or `"resume"`. Note
33
+ # the name: this sits *inside* the change's own `value` hash, so the full path
34
+ # reads `change.value.preferences.first.value`. Meta's naming, not ours.
35
+ attr_accessor :value
36
+
37
+ # @!attribute [rw] timestamp
38
+ # @return [Integer, String, nil] Unix seconds. Meta's example sends a number
39
+ # here, unlike the string timestamps on `messages`, so it is passed through
40
+ # as-is rather than coerced.
41
+ attr_accessor :timestamp
42
+
43
+ def initialize(
44
+ wa_id: nil, user_id: nil, parent_user_id: nil,
45
+ detail: nil, category: nil, value: nil, timestamp: nil
46
+ )
47
+ @wa_id = wa_id
48
+ @user_id = user_id
49
+ @parent_user_id = parent_user_id
50
+ @detail = detail
51
+ @category = category
52
+ @value = value
53
+ @timestamp = timestamp
54
+ end
55
+
56
+ class << self
57
+ # @param data [Hash, nil] A raw `user_preferences[]` entry.
58
+ # @return [Entry]
59
+ def deserialize(data)
60
+ data ||= {}
61
+
62
+ new(
63
+ wa_id: data["wa_id"],
64
+ user_id: data["user_id"],
65
+ parent_user_id: data["parent_user_id"],
66
+ detail: data["detail"],
67
+ category: data["category"],
68
+ value: data["value"],
69
+ timestamp: data["timestamp"]
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -3,43 +3,64 @@
3
3
  module Whatsapp
4
4
  module Webhook
5
5
  # User marketing message preference changes (e.g. a customer opting out of
6
- # marketing messages).
6
+ # marketing messages) — one of the fields with a confirmed, JSON-example-backed
7
+ # schema, alongside {Messages}, {UserIdUpdate}, and {BusinessUsernameUpdates}.
7
8
  #
8
- # Best-effort schema: Meta's public docs describe this field in one line with
9
- # no published JSON example. Validate against a real payload (App Dashboard
10
- # "send test payload") before relying on these attribute names in production.
11
- # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
+ # Structurally identical to {UserIdUpdate}: the value carries the notification's
10
+ # own identity (`messaging_product`, `metadata`, `contacts`) plus an array of
11
+ # entries nested under a repeat of the field's own name.
12
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
12
13
  class UserPreferences
13
- # @!attribute [rw] wa_id
14
+ # @!attribute [rw] messaging_product
14
15
  # @return [String, nil]
15
- attr_accessor :wa_id
16
+ attr_accessor :messaging_product
16
17
 
17
- # @!attribute [rw] detail
18
- # @return [String, nil]
19
- attr_accessor :detail
18
+ # @!attribute [rw] metadata
19
+ # @return [Metadata, nil]
20
+ attr_accessor :metadata
20
21
 
21
- # @!attribute [rw] value
22
- # @return [String, nil] e.g. `"stop"`, `"resume"`.
23
- attr_accessor :value
22
+ # @!attribute [rw] contacts
23
+ # @return [Array<Contact>] The users whose preferences changed, carrying the same
24
+ # BSUID identifiers as every other `contacts[]` entry — which is how a
25
+ # username-adopted user is identifiable here at all once their phone number
26
+ # stops being shared.
27
+ attr_accessor :contacts
24
28
 
25
- # @!attribute [rw] timestamp
26
- # @return [String, nil]
27
- attr_accessor :timestamp
29
+ # @!attribute [rw] preferences
30
+ # @return [Array<Entry>]
31
+ attr_accessor :preferences
32
+
33
+ # @!attribute [r] raw
34
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part
35
+ # of the supported API: {#preferences} reads `value["user_preferences"]` —
36
+ # entries nested under a repeat of the field's own name, which is what Meta's
37
+ # published example shows — so a key that drifts would otherwise leave
38
+ # `preferences` empty with no other trace. Reach for it when a typed accessor is
39
+ # unexpectedly `nil`, log what you find, and treat it as read-only: it is the
40
+ # object `JSON.parse` returned, not a copy.
41
+ attr_reader :raw
28
42
 
29
- def initialize(wa_id:, detail:, value:, timestamp:)
30
- @wa_id = wa_id
31
- @detail = detail
32
- @value = value
33
- @timestamp = timestamp
43
+ def initialize(messaging_product:, metadata:, contacts:, preferences:, raw: {})
44
+ @messaging_product = messaging_product
45
+ @metadata = metadata
46
+ @contacts = contacts
47
+ @preferences = preferences
48
+ @raw = raw
34
49
  end
35
50
 
36
51
  class << self
37
- # @param data [Hash] The raw `value` hash.
52
+ # @param data [Hash] The raw `user_preferences` field's `value` hash.
38
53
  # @return [UserPreferences]
39
54
  def deserialize(data)
40
55
  data ||= {}
41
56
 
42
- new(wa_id: data["wa_id"], detail: data["detail"], value: data["value"], timestamp: data["timestamp"])
57
+ new(
58
+ messaging_product: data["messaging_product"],
59
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
60
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
61
+ preferences: Array(data["user_preferences"]).map { |entry_data| Entry.deserialize(entry_data) },
62
+ raw: data
63
+ )
43
64
  end
44
65
  end
45
66
  end