ruby-whatsapp 0.3.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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +54 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +15 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +643 -0
  9. data/Rakefile +12 -0
  10. data/lib/ruby/whatsapp/client.rb +82 -0
  11. data/lib/ruby/whatsapp/configuration.rb +79 -0
  12. data/lib/ruby/whatsapp/instrumentation.rb +34 -0
  13. data/lib/ruby/whatsapp/media.rb +134 -0
  14. data/lib/ruby/whatsapp/message_templates/button/base.rb +26 -0
  15. data/lib/ruby/whatsapp/message_templates/button/copy_code.rb +59 -0
  16. data/lib/ruby/whatsapp/message_templates/button/otp/supported_app.rb +44 -0
  17. data/lib/ruby/whatsapp/message_templates/button/otp.rb +138 -0
  18. data/lib/ruby/whatsapp/message_templates/button/phone_number.rb +45 -0
  19. data/lib/ruby/whatsapp/message_templates/button/quick_reply.rb +38 -0
  20. data/lib/ruby/whatsapp/message_templates/button/url.rb +110 -0
  21. data/lib/ruby/whatsapp/message_templates/button.rb +56 -0
  22. data/lib/ruby/whatsapp/message_templates/categories.rb +47 -0
  23. data/lib/ruby/whatsapp/message_templates/component/base.rb +112 -0
  24. data/lib/ruby/whatsapp/message_templates/component/body.rb +139 -0
  25. data/lib/ruby/whatsapp/message_templates/component/buttons.rb +95 -0
  26. data/lib/ruby/whatsapp/message_templates/component/carousel/card.rb +100 -0
  27. data/lib/ruby/whatsapp/message_templates/component/carousel.rb +71 -0
  28. data/lib/ruby/whatsapp/message_templates/component/footer.rb +99 -0
  29. data/lib/ruby/whatsapp/message_templates/component/header.rb +188 -0
  30. data/lib/ruby/whatsapp/message_templates/component/limited_time_offer.rb +57 -0
  31. data/lib/ruby/whatsapp/message_templates/component.rb +62 -0
  32. data/lib/ruby/whatsapp/message_templates/component_set.rb +221 -0
  33. data/lib/ruby/whatsapp/message_templates/example.rb +148 -0
  34. data/lib/ruby/whatsapp/message_templates/library_template/body_inputs.rb +81 -0
  35. data/lib/ruby/whatsapp/message_templates/library_template/button_inputs.rb +146 -0
  36. data/lib/ruby/whatsapp/message_templates/library_template.rb +114 -0
  37. data/lib/ruby/whatsapp/message_templates/parameter_formats.rb +49 -0
  38. data/lib/ruby/whatsapp/message_templates/placeholders.rb +86 -0
  39. data/lib/ruby/whatsapp/message_templates/response/collection.rb +74 -0
  40. data/lib/ruby/whatsapp/message_templates/response/created.rb +65 -0
  41. data/lib/ruby/whatsapp/message_templates/response/node.rb +152 -0
  42. data/lib/ruby/whatsapp/message_templates/response/paging.rb +39 -0
  43. data/lib/ruby/whatsapp/message_templates/response/quality_score.rb +49 -0
  44. data/lib/ruby/whatsapp/message_templates/response/summary.rb +68 -0
  45. data/lib/ruby/whatsapp/message_templates/response.rb +22 -0
  46. data/lib/ruby/whatsapp/message_templates/statuses.rb +66 -0
  47. data/lib/ruby/whatsapp/message_templates/template.rb +180 -0
  48. data/lib/ruby/whatsapp/message_templates/value_object.rb +62 -0
  49. data/lib/ruby/whatsapp/message_templates.rb +283 -0
  50. data/lib/ruby/whatsapp/messages/address.rb +103 -0
  51. data/lib/ruby/whatsapp/messages/audio.rb +64 -0
  52. data/lib/ruby/whatsapp/messages/base.rb +46 -0
  53. data/lib/ruby/whatsapp/messages/contacts/address.rb +84 -0
  54. data/lib/ruby/whatsapp/messages/contacts/contact.rb +78 -0
  55. data/lib/ruby/whatsapp/messages/contacts/email.rb +49 -0
  56. data/lib/ruby/whatsapp/messages/contacts/name.rb +68 -0
  57. data/lib/ruby/whatsapp/messages/contacts/org.rb +41 -0
  58. data/lib/ruby/whatsapp/messages/contacts/phone.rb +59 -0
  59. data/lib/ruby/whatsapp/messages/contacts/url.rb +51 -0
  60. data/lib/ruby/whatsapp/messages/contacts.rb +37 -0
  61. data/lib/ruby/whatsapp/messages/document.rb +79 -0
  62. data/lib/ruby/whatsapp/messages/image.rb +72 -0
  63. data/lib/ruby/whatsapp/messages/interactive/base.rb +15 -0
  64. data/lib/ruby/whatsapp/messages/interactive/body.rb +39 -0
  65. data/lib/ruby/whatsapp/messages/interactive/footer.rb +40 -0
  66. data/lib/ruby/whatsapp/messages/interactive/header.rb +102 -0
  67. data/lib/ruby/whatsapp/messages/interactive/list_buttons/section/row.rb +66 -0
  68. data/lib/ruby/whatsapp/messages/interactive/list_buttons/section.rb +64 -0
  69. data/lib/ruby/whatsapp/messages/interactive/list_buttons.rb +63 -0
  70. data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button/quick_reply.rb +50 -0
  71. data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button.rb +47 -0
  72. data/lib/ruby/whatsapp/messages/interactive/media_carousel/card.rb +78 -0
  73. data/lib/ruby/whatsapp/messages/interactive/media_carousel.rb +50 -0
  74. data/lib/ruby/whatsapp/messages/interactive/product_carousel/card.rb +55 -0
  75. data/lib/ruby/whatsapp/messages/interactive/product_carousel.rb +50 -0
  76. data/lib/ruby/whatsapp/messages/interactive/reply_buttons/button.rb +59 -0
  77. data/lib/ruby/whatsapp/messages/interactive/reply_buttons.rb +47 -0
  78. data/lib/ruby/whatsapp/messages/interactive/url_button.rb +68 -0
  79. data/lib/ruby/whatsapp/messages/interactive.rb +112 -0
  80. data/lib/ruby/whatsapp/messages/location.rb +67 -0
  81. data/lib/ruby/whatsapp/messages/location_request.rb +41 -0
  82. data/lib/ruby/whatsapp/messages/mark_message_as_read.rb +42 -0
  83. data/lib/ruby/whatsapp/messages/reaction.rb +61 -0
  84. data/lib/ruby/whatsapp/messages/response/contacts.rb +36 -0
  85. data/lib/ruby/whatsapp/messages/response/messages.rb +27 -0
  86. data/lib/ruby/whatsapp/messages/response.rb +49 -0
  87. data/lib/ruby/whatsapp/messages/sticker.rb +62 -0
  88. data/lib/ruby/whatsapp/messages/template/component.rb +67 -0
  89. data/lib/ruby/whatsapp/messages/template/language.rb +55 -0
  90. data/lib/ruby/whatsapp/messages/template/parameter.rb +113 -0
  91. data/lib/ruby/whatsapp/messages/template.rb +62 -0
  92. data/lib/ruby/whatsapp/messages/text.rb +43 -0
  93. data/lib/ruby/whatsapp/messages/video.rb +68 -0
  94. data/lib/ruby/whatsapp/messages.rb +116 -0
  95. data/lib/ruby/whatsapp/railtie.rb +14 -0
  96. data/lib/ruby/whatsapp/request_error.rb +5 -0
  97. data/lib/ruby/whatsapp/response_handling.rb +44 -0
  98. data/lib/ruby/whatsapp/utils/language_codes.rb +96 -0
  99. data/lib/ruby/whatsapp/version.rb +5 -0
  100. data/lib/ruby/whatsapp/webhook/account_alerts.rb +58 -0
  101. data/lib/ruby/whatsapp/webhook/account_review_update.rb +27 -0
  102. data/lib/ruby/whatsapp/webhook/account_update/ban_info.rb +33 -0
  103. data/lib/ruby/whatsapp/webhook/account_update.rb +46 -0
  104. data/lib/ruby/whatsapp/webhook/automatic_events.rb +43 -0
  105. data/lib/ruby/whatsapp/webhook/business_capability_update.rb +36 -0
  106. data/lib/ruby/whatsapp/webhook/change.rb +61 -0
  107. data/lib/ruby/whatsapp/webhook/contact.rb +36 -0
  108. data/lib/ruby/whatsapp/webhook/entry.rb +35 -0
  109. data/lib/ruby/whatsapp/webhook/error.rb +47 -0
  110. data/lib/ruby/whatsapp/webhook/history.rb +54 -0
  111. data/lib/ruby/whatsapp/webhook/installer.rb +61 -0
  112. data/lib/ruby/whatsapp/webhook/message/audio.rb +28 -0
  113. data/lib/ruby/whatsapp/webhook/message/base.rb +61 -0
  114. data/lib/ruby/whatsapp/webhook/message/button.rb +33 -0
  115. data/lib/ruby/whatsapp/webhook/message/contacts/address.rb +68 -0
  116. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +68 -0
  117. data/lib/ruby/whatsapp/webhook/message/contacts/email.rb +35 -0
  118. data/lib/ruby/whatsapp/webhook/message/contacts/name.rb +62 -0
  119. data/lib/ruby/whatsapp/webhook/message/contacts/org.rb +40 -0
  120. data/lib/ruby/whatsapp/webhook/message/contacts/phone.rb +40 -0
  121. data/lib/ruby/whatsapp/webhook/message/contacts/url.rb +35 -0
  122. data/lib/ruby/whatsapp/webhook/message/contacts.rb +31 -0
  123. data/lib/ruby/whatsapp/webhook/message/context.rb +49 -0
  124. data/lib/ruby/whatsapp/webhook/message/document.rb +32 -0
  125. data/lib/ruby/whatsapp/webhook/message/image.rb +18 -0
  126. data/lib/ruby/whatsapp/webhook/message/interactive.rb +53 -0
  127. data/lib/ruby/whatsapp/webhook/message/location.rb +49 -0
  128. data/lib/ruby/whatsapp/webhook/message/media.rb +54 -0
  129. data/lib/ruby/whatsapp/webhook/message/order/product_item.rb +50 -0
  130. data/lib/ruby/whatsapp/webhook/message/order.rb +43 -0
  131. data/lib/ruby/whatsapp/webhook/message/reaction.rb +37 -0
  132. data/lib/ruby/whatsapp/webhook/message/referral.rb +80 -0
  133. data/lib/ruby/whatsapp/webhook/message/sticker.rb +32 -0
  134. data/lib/ruby/whatsapp/webhook/message/system.rb +49 -0
  135. data/lib/ruby/whatsapp/webhook/message/text.rb +28 -0
  136. data/lib/ruby/whatsapp/webhook/message/unknown.rb +39 -0
  137. data/lib/ruby/whatsapp/webhook/message/video.rb +18 -0
  138. data/lib/ruby/whatsapp/webhook/message.rb +39 -0
  139. data/lib/ruby/whatsapp/webhook/message_template_components_update.rb +53 -0
  140. data/lib/ruby/whatsapp/webhook/message_template_quality_update.rb +59 -0
  141. data/lib/ruby/whatsapp/webhook/message_template_status_update.rb +54 -0
  142. data/lib/ruby/whatsapp/webhook/messages.rb +55 -0
  143. data/lib/ruby/whatsapp/webhook/metadata.rb +35 -0
  144. data/lib/ruby/whatsapp/webhook/notification.rb +36 -0
  145. data/lib/ruby/whatsapp/webhook/partner_solutions.rb +36 -0
  146. data/lib/ruby/whatsapp/webhook/payment_configuration_update.rb +51 -0
  147. data/lib/ruby/whatsapp/webhook/phone_number_name_update.rb +51 -0
  148. data/lib/ruby/whatsapp/webhook/phone_number_quality_update.rb +45 -0
  149. data/lib/ruby/whatsapp/webhook/security.rb +41 -0
  150. data/lib/ruby/whatsapp/webhook/signature.rb +28 -0
  151. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +38 -0
  152. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -0
  153. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +48 -0
  154. data/lib/ruby/whatsapp/webhook/status/conversation.rb +42 -0
  155. data/lib/ruby/whatsapp/webhook/status/pricing.rb +38 -0
  156. data/lib/ruby/whatsapp/webhook/status.rb +65 -0
  157. data/lib/ruby/whatsapp/webhook/template_category_update.rb +64 -0
  158. data/lib/ruby/whatsapp/webhook/templates/webhooks_controller.rb.tt +31 -0
  159. data/lib/ruby/whatsapp/webhook/unknown_field.rb +37 -0
  160. data/lib/ruby/whatsapp/webhook/user_preferences.rb +47 -0
  161. data/lib/ruby/whatsapp/webhook/verification.rb +27 -0
  162. data/lib/ruby/whatsapp/webhook.rb +8 -0
  163. data/lib/ruby/whatsapp.rb +49 -0
  164. data/lib/tasks/whatsapp.rake +10 -0
  165. data/sig/ruby/whatsapp.rbs +6 -0
  166. metadata +267 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
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.
8
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
+ class Messages
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] messages
23
+ # @return [Array<Message::Base>]
24
+ attr_accessor :messages
25
+
26
+ # @!attribute [rw] statuses
27
+ # @return [Array<Status>]
28
+ attr_accessor :statuses
29
+
30
+ def initialize(messaging_product:, metadata:, contacts:, messages:, statuses:)
31
+ @messaging_product = messaging_product
32
+ @metadata = metadata
33
+ @contacts = contacts
34
+ @messages = messages
35
+ @statuses = statuses
36
+ end
37
+
38
+ class << self
39
+ # @param data [Hash] The raw `messages` field's `value` hash.
40
+ # @return [Messages]
41
+ def deserialize(data)
42
+ data ||= {}
43
+
44
+ new(
45
+ messaging_product: data["messaging_product"],
46
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
47
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
48
+ messages: Array(data["messages"]).map { |message_data| Message.deserialize(message_data) },
49
+ statuses: Array(data["statuses"]).map { |status_data| Status.deserialize(status_data) }
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # The `metadata` object present on most webhook `value` payloads, identifying
6
+ # which of your business phone numbers the notification is about.
7
+ class Metadata
8
+ # @!attribute [rw] display_phone_number
9
+ # @return [String, nil]
10
+ attr_accessor :display_phone_number
11
+
12
+ # @!attribute [rw] phone_number_id
13
+ # @return [String, nil]
14
+ attr_accessor :phone_number_id
15
+
16
+ def initialize(display_phone_number:, phone_number_id:)
17
+ @display_phone_number = display_phone_number
18
+ @phone_number_id = phone_number_id
19
+ end
20
+
21
+ class << self
22
+ # @param data [Hash] The raw `metadata` hash.
23
+ # @return [Metadata]
24
+ def deserialize(data)
25
+ data ||= {}
26
+
27
+ new(
28
+ display_phone_number: data["display_phone_number"],
29
+ phone_number_id: data["phone_number_id"]
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Top-level deserialization entry point for an incoming webhook POST body.
6
+ # @example
7
+ # Whatsapp::Webhook::Notification.deserialize(JSON.parse(request.body.read))
8
+ class Notification
9
+ # @!attribute [rw] object
10
+ # @return [String]
11
+ attr_accessor :object
12
+
13
+ # @!attribute [rw] entry
14
+ # @return [Array<Entry>]
15
+ attr_accessor :entry
16
+
17
+ def initialize(object:, entry:)
18
+ @object = object
19
+ @entry = entry
20
+ end
21
+
22
+ class << self
23
+ # @param data [Hash] The parsed JSON request body.
24
+ # @return [Notification]
25
+ def deserialize(data)
26
+ data ||= {}
27
+
28
+ new(
29
+ object: data["object"],
30
+ entry: Array(data["entry"]).map { |entry_data| Entry.deserialize(entry_data) }
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Multi-Partner Solution status changes.
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example. Validate against a real payload (App Dashboard
9
+ # "send test payload") before relying on these attribute names in production.
10
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
11
+ class PartnerSolutions
12
+ # @!attribute [rw] solution_id
13
+ # @return [String, nil]
14
+ attr_accessor :solution_id
15
+
16
+ # @!attribute [rw] event
17
+ # @return [String, nil]
18
+ attr_accessor :event
19
+
20
+ def initialize(solution_id:, event:)
21
+ @solution_id = solution_id
22
+ @event = event
23
+ end
24
+
25
+ class << self
26
+ # @param data [Hash] The raw `value` hash.
27
+ # @return [PartnerSolutions]
28
+ def deserialize(data)
29
+ data ||= {}
30
+
31
+ new(solution_id: data["solution_id"], event: data["event"])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Payment configuration changes (India/Brazil).
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example. Validate against a real payload (App Dashboard
9
+ # "send test payload") before relying on these attribute names in production.
10
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
11
+ class PaymentConfigurationUpdate
12
+ # @!attribute [rw] configuration_name
13
+ # @return [String, nil]
14
+ attr_accessor :configuration_name
15
+
16
+ # @!attribute [rw] provider_name
17
+ # @return [String, nil]
18
+ attr_accessor :provider_name
19
+
20
+ # @!attribute [rw] provider_mid
21
+ # @return [String, nil]
22
+ attr_accessor :provider_mid
23
+
24
+ # @!attribute [rw] status
25
+ # @return [String, nil]
26
+ attr_accessor :status
27
+
28
+ def initialize(configuration_name:, provider_name:, provider_mid:, status:)
29
+ @configuration_name = configuration_name
30
+ @provider_name = provider_name
31
+ @provider_mid = provider_mid
32
+ @status = status
33
+ end
34
+
35
+ class << self
36
+ # @param data [Hash] The raw `value` hash.
37
+ # @return [PaymentConfigurationUpdate]
38
+ def deserialize(data)
39
+ data ||= {}
40
+
41
+ new(
42
+ configuration_name: data["configuration_name"],
43
+ provider_name: data["provider_name"],
44
+ provider_mid: data["provider_mid"],
45
+ status: data["status"]
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Display name verification outcomes for a business phone number.
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example. Validate against a real payload (App Dashboard
9
+ # "send test payload") before relying on these attribute names in production.
10
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
11
+ class PhoneNumberNameUpdate
12
+ # @!attribute [rw] phone_number
13
+ # @return [String, nil]
14
+ attr_accessor :phone_number
15
+
16
+ # @!attribute [rw] decision
17
+ # @return [String, nil] e.g. `"APPROVED"`, `"REJECTED"`.
18
+ attr_accessor :decision
19
+
20
+ # @!attribute [rw] requested_verified_name
21
+ # @return [String, nil]
22
+ attr_accessor :requested_verified_name
23
+
24
+ # @!attribute [rw] rejection_reason
25
+ # @return [String, nil]
26
+ attr_accessor :rejection_reason
27
+
28
+ def initialize(phone_number:, decision:, requested_verified_name:, rejection_reason: nil)
29
+ @phone_number = phone_number
30
+ @decision = decision
31
+ @requested_verified_name = requested_verified_name
32
+ @rejection_reason = rejection_reason
33
+ end
34
+
35
+ class << self
36
+ # @param data [Hash] The raw `value` hash.
37
+ # @return [PhoneNumberNameUpdate]
38
+ def deserialize(data)
39
+ data ||= {}
40
+
41
+ new(
42
+ phone_number: data["phone_number"],
43
+ decision: data["decision"],
44
+ requested_verified_name: data["requested_verified_name"],
45
+ rejection_reason: data["rejection_reason"]
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Business phone throughput/quality level changes.
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example. Validate against a real payload (App Dashboard
9
+ # "send test payload") before relying on these attribute names in production.
10
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
11
+ class PhoneNumberQualityUpdate
12
+ # @!attribute [rw] display_phone_number
13
+ # @return [String, nil]
14
+ attr_accessor :display_phone_number
15
+
16
+ # @!attribute [rw] event
17
+ # @return [String, nil] e.g. `"UPGRADE"`, `"DOWNGRADE"`.
18
+ attr_accessor :event
19
+
20
+ # @!attribute [rw] current_limit
21
+ # @return [String, nil] e.g. `"TIER_50"`, `"TIER_250"`, `"TIER_1K"`.
22
+ attr_accessor :current_limit
23
+
24
+ def initialize(display_phone_number:, event:, current_limit:)
25
+ @display_phone_number = display_phone_number
26
+ @event = event
27
+ @current_limit = current_limit
28
+ end
29
+
30
+ class << self
31
+ # @param data [Hash] The raw `value` hash.
32
+ # @return [PhoneNumberQualityUpdate]
33
+ def deserialize(data)
34
+ data ||= {}
35
+
36
+ new(
37
+ display_phone_number: data["display_phone_number"],
38
+ event: data["event"],
39
+ current_limit: data["current_limit"]
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Phone number security settings modifications (e.g. two-step verification changes).
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example. Validate against a real payload (App Dashboard
9
+ # "send test payload") before relying on these attribute names in production.
10
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
11
+ class Security
12
+ # @!attribute [rw] display_phone_number
13
+ # @return [String, nil]
14
+ attr_accessor :display_phone_number
15
+
16
+ # @!attribute [rw] event
17
+ # @return [String, nil]
18
+ attr_accessor :event
19
+
20
+ # @!attribute [rw] requester
21
+ # @return [String, nil]
22
+ attr_accessor :requester
23
+
24
+ def initialize(display_phone_number:, event:, requester: nil)
25
+ @display_phone_number = display_phone_number
26
+ @event = event
27
+ @requester = requester
28
+ end
29
+
30
+ class << self
31
+ # @param data [Hash] The raw `value` hash.
32
+ # @return [Security]
33
+ def deserialize(data)
34
+ data ||= {}
35
+
36
+ new(display_phone_number: data["display_phone_number"], event: data["event"], requester: data["requester"])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Verifies the `X-Hub-Signature-256` header Meta attaches to every webhook POST,
6
+ # an HMAC-SHA256 of the raw request body keyed by the app secret.
7
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
8
+ class Signature
9
+ PREFIX = "sha256="
10
+
11
+ class << self
12
+ # @param payload [String] The raw (unparsed) request body.
13
+ # @param header [String, nil] The `X-Hub-Signature-256` header value.
14
+ # @param app_secret [String, nil] The secret to verify against. Defaults to the
15
+ # globally configured secret; pass an explicit value for multi-tenant apps where
16
+ # each account has its own Meta App (and therefore its own secret).
17
+ # @return [Boolean] Whether the header matches the computed signature.
18
+ def valid?(payload:, header:, app_secret: Whatsapp.configuration.app_secret)
19
+ return false if header.nil? || header.empty?
20
+ return false if app_secret.nil? || app_secret.empty?
21
+
22
+ expected = PREFIX + OpenSSL::HMAC.hexdigest("SHA256", app_secret, payload)
23
+ ActiveSupport::SecurityUtils.secure_compare(expected, header)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class SmbAppStateSync
6
+ # A single contact-sync entry within an `smb_app_state_sync` notification.
7
+ class StateSync
8
+ # @!attribute [rw] type
9
+ # @return [String, nil]
10
+ attr_accessor :type
11
+
12
+ # @!attribute [rw] action
13
+ # @return [String, nil] e.g. `"add"`, `"remove"`, `"update"`.
14
+ attr_accessor :action
15
+
16
+ # @!attribute [rw] contact
17
+ # @return [Hash] Left untyped — Meta's docs don't publish this entry's schema.
18
+ attr_accessor :contact
19
+
20
+ def initialize(type:, action:, contact:)
21
+ @type = type
22
+ @action = action
23
+ @contact = contact
24
+ end
25
+
26
+ class << self
27
+ # @param data [Hash] A raw `state_sync[]` entry.
28
+ # @return [StateSync]
29
+ def deserialize(data)
30
+ data ||= {}
31
+
32
+ new(type: data["type"], action: data["action"], contact: data["contact"] || {})
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Contact synchronization for onboarded WhatsApp Business app users.
6
+ #
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
13
+ class SmbAppStateSync
14
+ # @!attribute [rw] state_sync
15
+ # @return [Array<StateSync>]
16
+ attr_accessor :state_sync
17
+
18
+ def initialize(state_sync:)
19
+ @state_sync = state_sync
20
+ end
21
+
22
+ class << self
23
+ # @param data [Hash] The raw `value` hash.
24
+ # @return [SmbAppStateSync]
25
+ def deserialize(data)
26
+ data ||= {}
27
+
28
+ new(state_sync: Array(data["state_sync"]).map { |entry| StateSync.deserialize(entry) })
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
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.
7
+ #
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
14
+ class SmbMessageEchoes
15
+ # @!attribute [rw] messaging_product
16
+ # @return [String, nil]
17
+ attr_accessor :messaging_product
18
+
19
+ # @!attribute [rw] metadata
20
+ # @return [Metadata, nil]
21
+ attr_accessor :metadata
22
+
23
+ # @!attribute [rw] message_echoes
24
+ # @return [Array<Message::Base>]
25
+ attr_accessor :message_echoes
26
+
27
+ def initialize(messaging_product:, metadata:, message_echoes:)
28
+ @messaging_product = messaging_product
29
+ @metadata = metadata
30
+ @message_echoes = message_echoes
31
+ end
32
+
33
+ class << self
34
+ # @param data [Hash] The raw `value` hash.
35
+ # @return [SmbMessageEchoes]
36
+ def deserialize(data)
37
+ data ||= {}
38
+
39
+ new(
40
+ messaging_product: data["messaging_product"],
41
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
42
+ message_echoes: Array(data["message_echoes"]).map { |echo_data| Message.deserialize(echo_data) }
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Status
6
+ # The conversation window a status update was billed/attributed to.
7
+ class Conversation
8
+ # @!attribute [rw] id
9
+ # @return [String, nil]
10
+ attr_accessor :id
11
+
12
+ # @!attribute [rw] origin_type
13
+ # @return [String, nil] e.g. `"service"`, `"marketing"`, `"utility"`, `"authentication"`.
14
+ attr_accessor :origin_type
15
+
16
+ # @!attribute [rw] expiration_timestamp
17
+ # @return [String, nil]
18
+ attr_accessor :expiration_timestamp
19
+
20
+ def initialize(id:, origin_type:, expiration_timestamp: nil)
21
+ @id = id
22
+ @origin_type = origin_type
23
+ @expiration_timestamp = expiration_timestamp
24
+ end
25
+
26
+ class << self
27
+ # @param data [Hash] The raw `conversation` hash.
28
+ # @return [Conversation]
29
+ def deserialize(data)
30
+ data ||= {}
31
+
32
+ new(
33
+ id: data["id"],
34
+ origin_type: data.dig("origin", "type"),
35
+ expiration_timestamp: data["expiration_timestamp"]
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Status
6
+ # The billing details attached to a status update.
7
+ class Pricing
8
+ # @!attribute [rw] billable
9
+ # @return [Boolean, nil]
10
+ attr_accessor :billable
11
+
12
+ # @!attribute [rw] pricing_model
13
+ # @return [String, nil]
14
+ attr_accessor :pricing_model
15
+
16
+ # @!attribute [rw] category
17
+ # @return [String, nil] e.g. `"service"`, `"marketing"`, `"utility"`, `"authentication"`.
18
+ attr_accessor :category
19
+
20
+ def initialize(billable:, pricing_model:, category:)
21
+ @billable = billable
22
+ @pricing_model = pricing_model
23
+ @category = category
24
+ end
25
+
26
+ class << self
27
+ # @param data [Hash] The raw `pricing` hash.
28
+ # @return [Pricing]
29
+ def deserialize(data)
30
+ data ||= {}
31
+
32
+ new(billable: data["billable"], pricing_model: data["pricing_model"], category: data["category"])
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # A single `statuses[]` entry from the `messages` webhook field — a delivery
6
+ # status update (sent/delivered/read/failed) for a message you sent.
7
+ class Status
8
+ # @!attribute [rw] id
9
+ # @return [String, nil] The wamid of the message this status is about.
10
+ attr_accessor :id
11
+
12
+ # @!attribute [rw] status
13
+ # @return [String, nil] `"sent"`, `"delivered"`, `"read"`, or `"failed"`.
14
+ attr_accessor :status
15
+
16
+ # @!attribute [rw] timestamp
17
+ # @return [String, nil]
18
+ attr_accessor :timestamp
19
+
20
+ # @!attribute [rw] recipient_id
21
+ # @return [String, nil]
22
+ attr_accessor :recipient_id
23
+
24
+ # @!attribute [rw] conversation
25
+ # @return [Conversation, nil]
26
+ attr_accessor :conversation
27
+
28
+ # @!attribute [rw] pricing
29
+ # @return [Pricing, nil]
30
+ attr_accessor :pricing
31
+
32
+ # @!attribute [rw] errors
33
+ # @return [Array<Error>]
34
+ attr_accessor :errors
35
+
36
+ def initialize(id:, status:, timestamp:, recipient_id:, conversation: nil, pricing: nil, errors: [])
37
+ @id = id
38
+ @status = status
39
+ @timestamp = timestamp
40
+ @recipient_id = recipient_id
41
+ @conversation = conversation
42
+ @pricing = pricing
43
+ @errors = errors
44
+ end
45
+
46
+ class << self
47
+ # @param data [Hash] A raw `statuses[]` entry.
48
+ # @return [Status]
49
+ def deserialize(data)
50
+ data ||= {}
51
+
52
+ new(
53
+ id: data["id"],
54
+ status: data["status"],
55
+ timestamp: data["timestamp"],
56
+ recipient_id: data["recipient_id"],
57
+ conversation: data["conversation"] ? Conversation.deserialize(data["conversation"]) : nil,
58
+ pricing: data["pricing"] ? Pricing.deserialize(data["pricing"]) : nil,
59
+ errors: Array(data["errors"]).map { |error_data| Error.deserialize(error_data) }
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end