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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Utils
5
+ # Utility module for WhatsApp supported language codes
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages
7
+ module LanguageCodes
8
+ CODES = %w[
9
+ af
10
+ sq
11
+ ar
12
+ az
13
+ bn
14
+ bg
15
+ ca
16
+ zh_CN
17
+ zh_HK
18
+ zh_TW
19
+ hr
20
+ cs
21
+ da
22
+ nl
23
+ en
24
+ en_GB
25
+ en_US
26
+ et
27
+ fil
28
+ fi
29
+ fr
30
+ ka
31
+ de
32
+ el
33
+ gu
34
+ ha
35
+ he
36
+ hi
37
+ hu
38
+ id
39
+ ga
40
+ it
41
+ ja
42
+ kn
43
+ kk
44
+ rw_RW
45
+ ko
46
+ ky_KG
47
+ lo
48
+ lv
49
+ lt
50
+ mk
51
+ ms
52
+ ml
53
+ mr
54
+ nb
55
+ fa
56
+ pl
57
+ pt_BR
58
+ pt_PT
59
+ pa
60
+ ro
61
+ ru
62
+ sr
63
+ sk
64
+ sl
65
+ es
66
+ es_AR
67
+ es_ES
68
+ es_MX
69
+ sw
70
+ sv
71
+ ta
72
+ te
73
+ th
74
+ tr
75
+ uk
76
+ ur
77
+ uz
78
+ vi
79
+ zu
80
+ ].freeze
81
+
82
+ # Check if a language code is valid
83
+ # @param code [String] The language code to validate
84
+ # @return [Boolean] true if the code is valid
85
+ def self.valid?(code)
86
+ CODES.include?(code.to_s)
87
+ end
88
+
89
+ # Get all supported language codes
90
+ # @return [Array<String>] Array of all supported language codes
91
+ def self.all
92
+ CODES
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ VERSION = "0.3.0"
5
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Notifications of messaging limit, business profile, and Official Business
6
+ # Account status changes.
7
+ #
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
12
+ class AccountAlerts
13
+ # @!attribute [rw] entity_type
14
+ # @return [String, nil]
15
+ attr_accessor :entity_type
16
+
17
+ # @!attribute [rw] entity_id
18
+ # @return [String, nil]
19
+ attr_accessor :entity_id
20
+
21
+ # @!attribute [rw] alert_severity
22
+ # @return [String, nil]
23
+ attr_accessor :alert_severity
24
+
25
+ # @!attribute [rw] alert_status
26
+ # @return [String, nil]
27
+ attr_accessor :alert_status
28
+
29
+ # @!attribute [rw] alert_description
30
+ # @return [String, nil]
31
+ attr_accessor :alert_description
32
+
33
+ def initialize(entity_type:, entity_id:, alert_severity:, alert_status:, alert_description:)
34
+ @entity_type = entity_type
35
+ @entity_id = entity_id
36
+ @alert_severity = alert_severity
37
+ @alert_status = alert_status
38
+ @alert_description = alert_description
39
+ end
40
+
41
+ class << self
42
+ # @param data [Hash] The raw `value` hash.
43
+ # @return [AccountAlerts]
44
+ def deserialize(data)
45
+ data ||= {}
46
+
47
+ new(
48
+ entity_type: data["entity_type"],
49
+ entity_id: data["entity_id"],
50
+ alert_severity: data["alert_severity"],
51
+ alert_status: data["alert_status"],
52
+ alert_description: data["alert_description"]
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Policy guideline review outcomes for WhatsApp Business Accounts.
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
7
+ class AccountReviewUpdate
8
+ # @!attribute [rw] decision
9
+ # @return [String, nil] e.g. `"APPROVED"`, `"REJECTED"`.
10
+ attr_accessor :decision
11
+
12
+ def initialize(decision:)
13
+ @decision = decision
14
+ end
15
+
16
+ class << self
17
+ # @param data [Hash] The raw `value` hash.
18
+ # @return [AccountReviewUpdate]
19
+ def deserialize(data)
20
+ data ||= {}
21
+
22
+ new(decision: data["decision"])
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class AccountUpdate
6
+ # The ban details attached to an `account_update` notification.
7
+ class BanInfo
8
+ # @!attribute [rw] waba_ban_state
9
+ # @return [String, nil]
10
+ attr_accessor :waba_ban_state
11
+
12
+ # @!attribute [rw] waba_ban_date
13
+ # @return [String, nil]
14
+ attr_accessor :waba_ban_date
15
+
16
+ def initialize(waba_ban_state:, waba_ban_date:)
17
+ @waba_ban_state = waba_ban_state
18
+ @waba_ban_date = waba_ban_date
19
+ end
20
+
21
+ class << self
22
+ # @param data [Hash] The raw `ban_info` hash.
23
+ # @return [BanInfo]
24
+ def deserialize(data)
25
+ data ||= {}
26
+
27
+ new(waba_ban_state: data["waba_ban_state"], waba_ban_date: data["waba_ban_date"])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Partner-led verification, authentication rates, primary location, policy
6
+ # violations, offboarding, reconnection, or deletion of a WhatsApp Business Account.
7
+ #
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
12
+ class AccountUpdate
13
+ # @!attribute [rw] phone_number
14
+ # @return [String, nil]
15
+ attr_accessor :phone_number
16
+
17
+ # @!attribute [rw] event
18
+ # @return [String, nil] e.g. `"VERIFIED_ACCOUNT"`, `"DISABLED_UPDATE"`, `"PARTNER_APP_INSTALLED"`.
19
+ attr_accessor :event
20
+
21
+ # @!attribute [rw] ban_info
22
+ # @return [BanInfo, nil]
23
+ attr_accessor :ban_info
24
+
25
+ def initialize(phone_number:, event:, ban_info: nil)
26
+ @phone_number = phone_number
27
+ @event = event
28
+ @ban_info = ban_info
29
+ end
30
+
31
+ class << self
32
+ # @param data [Hash] The raw `value` hash.
33
+ # @return [AccountUpdate]
34
+ def deserialize(data)
35
+ data ||= {}
36
+
37
+ new(
38
+ phone_number: data["phone_number"],
39
+ event: data["event"],
40
+ ban_info: data["ban_info"] ? BanInfo.deserialize(data["ban_info"]) : nil
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Purchase or lead event detection from Click to WhatsApp ad conversations.
6
+ #
7
+ # Best-effort schema: Meta's public docs describe this field in one line with
8
+ # no published JSON example; `event_data`'s nested shape isn't confidently
9
+ # typeable from that description alone, so it's kept as a raw hash rather than
10
+ # guessing further. Validate against a real payload before relying on this in
11
+ # production.
12
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
13
+ class AutomaticEvents
14
+ # @!attribute [rw] event_type
15
+ # @return [String, nil] e.g. `"purchase"`, `"lead"`.
16
+ attr_accessor :event_type
17
+
18
+ # @!attribute [rw] message_id
19
+ # @return [String, nil]
20
+ attr_accessor :message_id
21
+
22
+ # @!attribute [rw] event_data
23
+ # @return [Hash]
24
+ attr_accessor :event_data
25
+
26
+ def initialize(event_type:, message_id:, event_data:)
27
+ @event_type = event_type
28
+ @message_id = message_id
29
+ @event_data = event_data
30
+ end
31
+
32
+ class << self
33
+ # @param data [Hash] The raw `value` hash.
34
+ # @return [AutomaticEvents]
35
+ def deserialize(data)
36
+ data ||= {}
37
+
38
+ new(event_type: data["event_type"], message_id: data["message_id"], event_data: data["event_data"] || {})
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Messaging limits, phone number limits, and capability changes for a
6
+ # WhatsApp Business Account.
7
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
8
+ class BusinessCapabilityUpdate
9
+ # @!attribute [rw] max_daily_conversation_per_phone
10
+ # @return [Integer, nil]
11
+ attr_accessor :max_daily_conversation_per_phone
12
+
13
+ # @!attribute [rw] max_phone_numbers_per_business
14
+ # @return [Integer, nil]
15
+ attr_accessor :max_phone_numbers_per_business
16
+
17
+ def initialize(max_daily_conversation_per_phone:, max_phone_numbers_per_business:)
18
+ @max_daily_conversation_per_phone = max_daily_conversation_per_phone
19
+ @max_phone_numbers_per_business = max_phone_numbers_per_business
20
+ end
21
+
22
+ class << self
23
+ # @param data [Hash] The raw `value` hash.
24
+ # @return [BusinessCapabilityUpdate]
25
+ def deserialize(data)
26
+ data ||= {}
27
+
28
+ new(
29
+ max_daily_conversation_per_phone: data["max_daily_conversation_per_phone"],
30
+ max_phone_numbers_per_business: data["max_phone_numbers_per_business"]
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # A single `entry[].changes[]` item: `field` names which of Meta's ~19 documented
6
+ # webhook notification types this is, `value` is the deserialized payload for it.
7
+ #
8
+ # `FIELDS` is populated incrementally as each field's value class is implemented
9
+ # (see Whatsapp::Webhook::CLAUDE.md) — never `const_get` on the field name, which
10
+ # comes straight from the internet; a field not yet in the registry falls back to
11
+ # {UnknownField} rather than raising.
12
+ class Change
13
+ FIELDS = {
14
+ messages: Messages,
15
+ account_alerts: AccountAlerts,
16
+ account_review_update: AccountReviewUpdate,
17
+ account_update: AccountUpdate,
18
+ automatic_events: AutomaticEvents,
19
+ business_capability_update: BusinessCapabilityUpdate,
20
+ history: History,
21
+ message_template_components_update: MessageTemplateComponentsUpdate,
22
+ message_template_quality_update: MessageTemplateQualityUpdate,
23
+ message_template_status_update: MessageTemplateStatusUpdate,
24
+ partner_solutions: PartnerSolutions,
25
+ payment_configuration_update: PaymentConfigurationUpdate,
26
+ phone_number_name_update: PhoneNumberNameUpdate,
27
+ phone_number_quality_update: PhoneNumberQualityUpdate,
28
+ security: Security,
29
+ smb_app_state_sync: SmbAppStateSync,
30
+ smb_message_echoes: SmbMessageEchoes,
31
+ template_category_update: TemplateCategoryUpdate,
32
+ user_preferences: UserPreferences,
33
+ }.freeze
34
+
35
+ # @!attribute [rw] field
36
+ # @return [String]
37
+ attr_accessor :field
38
+
39
+ # @!attribute [rw] value
40
+ # @return [Object] One of the classes registered in {FIELDS}, or {UnknownField}.
41
+ attr_accessor :value
42
+
43
+ def initialize(field:, value:)
44
+ @field = field
45
+ @value = value
46
+ end
47
+
48
+ class << self
49
+ # @param data [Hash] A raw `changes[]` entry.
50
+ # @return [Change]
51
+ def deserialize(data)
52
+ data ||= {}
53
+ field = data["field"]
54
+ klass = FIELDS.fetch(field.to_s.to_sym) { UnknownField }
55
+
56
+ new(field:, value: klass.deserialize(data["value"]))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # A value-level `contacts[]` entry — identifies the WhatsApp user tied to a
6
+ # notification (distinct from `Message::Contacts`, the inbound "share a contact
7
+ # card" message type, which has its own richer shape).
8
+ class Contact
9
+ # @!attribute [rw] profile_name
10
+ # @return [String, nil]
11
+ attr_accessor :profile_name
12
+
13
+ # @!attribute [rw] wa_id
14
+ # @return [String, nil]
15
+ attr_accessor :wa_id
16
+
17
+ def initialize(profile_name:, wa_id:)
18
+ @profile_name = profile_name
19
+ @wa_id = wa_id
20
+ end
21
+
22
+ class << self
23
+ # @param data [Hash] A raw `contacts[]` entry.
24
+ # @return [Contact]
25
+ def deserialize(data)
26
+ data ||= {}
27
+
28
+ new(
29
+ profile_name: data.dig("profile", "name"),
30
+ wa_id: data["wa_id"]
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # A single `entry[]` item — one WhatsApp Business Account, with one or more
6
+ # `changes[]` (each a distinct notification).
7
+ class Entry
8
+ # @!attribute [rw] id
9
+ # @return [String]
10
+ attr_accessor :id
11
+
12
+ # @!attribute [rw] changes
13
+ # @return [Array<Change>]
14
+ attr_accessor :changes
15
+
16
+ def initialize(id:, changes:)
17
+ @id = id
18
+ @changes = changes
19
+ end
20
+
21
+ class << self
22
+ # @param data [Hash] A raw `entry[]` item.
23
+ # @return [Entry]
24
+ def deserialize(data)
25
+ data ||= {}
26
+
27
+ new(
28
+ id: data["id"],
29
+ changes: Array(data["changes"]).map { |change_data| Change.deserialize(change_data) }
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # An error entry, present on message/status payloads that couldn't be fully
6
+ # processed (e.g. an unsupported message type, or a failed delivery).
7
+ class Error
8
+ # @!attribute [rw] code
9
+ # @return [Integer, nil]
10
+ attr_accessor :code
11
+
12
+ # @!attribute [rw] title
13
+ # @return [String, nil]
14
+ attr_accessor :title
15
+
16
+ # @!attribute [rw] message
17
+ # @return [String, nil]
18
+ attr_accessor :message
19
+
20
+ # @!attribute [rw] details
21
+ # @return [String, nil]
22
+ attr_accessor :details
23
+
24
+ def initialize(code:, title:, message:, details: nil)
25
+ @code = code
26
+ @title = title
27
+ @message = message
28
+ @details = details
29
+ end
30
+
31
+ class << self
32
+ # @param data [Hash] A raw `errors[]` entry.
33
+ # @return [Error]
34
+ def deserialize(data)
35
+ data ||= {}
36
+
37
+ new(
38
+ code: data["code"],
39
+ title: data["title"],
40
+ message: data["message"],
41
+ details: data.dig("error_data", "details")
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Chat history synchronization for onboarded business customers.
6
+ #
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
13
+ 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
35
+ end
36
+
37
+ class << self
38
+ # @param data [Hash] The raw `value` hash.
39
+ # @return [History]
40
+ def deserialize(data)
41
+ data ||= {}
42
+ metadata = data["metadata"] || {}
43
+
44
+ new(
45
+ phase: metadata["phase"],
46
+ chunk_order: metadata["chunk_order"],
47
+ progress: metadata["progress"],
48
+ threads: Array(data["threads"])
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ # Copies a personalizable webhook controller into a host Rails app. Plain
6
+ # Ruby with no Rails dependency of its own, so it can be unit tested against
7
+ # a plain directory — `lib/tasks/whatsapp.rake` is the thin Rails-facing
8
+ # wrapper around this.
9
+ class Installer
10
+ TEMPLATE_PATH = File.expand_path("templates/webhooks_controller.rb.tt", __dir__)
11
+ DESTINATION = "app/controllers/whatsapp/webhooks_controller.rb"
12
+
13
+ NEXT_STEPS = <<~TEXT
14
+ Next steps:
15
+
16
+ 1. Add these routes to config/routes.rb:
17
+
18
+ get "/whatsapp/webhooks", to: "whatsapp/webhooks#verify"
19
+ post "/whatsapp/webhooks", to: "whatsapp/webhooks#receive"
20
+
21
+ 2. Configure your verify token and app secret, e.g. in config/initializers/whatsapp.rb:
22
+
23
+ Whatsapp.configure do |config|
24
+ config.verify_token = Rails.application.credentials.whatsapp_verify_token
25
+ config.app_secret = Rails.application.credentials.whatsapp_app_secret
26
+ end
27
+
28
+ 3. Personalize app/controllers/whatsapp/webhooks_controller.rb — it's your file now.
29
+ TEXT
30
+
31
+ class << self
32
+ # @param root [String] The host app's root directory.
33
+ # @param output [IO] Where to print progress and next steps.
34
+ # @return [Symbol] `:created` or `:skipped` (an existing controller is never overwritten).
35
+ def call(root: Dir.pwd, output: $stdout)
36
+ destination = File.join(root, DESTINATION)
37
+ result = write_controller(destination, output)
38
+
39
+ output.puts
40
+ output.puts NEXT_STEPS
41
+
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def write_controller(destination, output)
48
+ if File.exist?(destination)
49
+ output.puts "Skipped: #{DESTINATION} already exists."
50
+ return :skipped
51
+ end
52
+
53
+ FileUtils.mkdir_p(File.dirname(destination))
54
+ FileUtils.cp(TEMPLATE_PATH, destination)
55
+ output.puts "Created #{DESTINATION}"
56
+ :created
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Message
6
+ # An inbound audio message (voice note or audio clip).
7
+ class Audio < Media
8
+ # @!attribute [rw] voice
9
+ # @return [Boolean, nil] Whether this was recorded as a voice note.
10
+ attr_accessor :voice
11
+
12
+ def initialize(voice: nil, **media_and_base_attributes)
13
+ super(**media_and_base_attributes)
14
+
15
+ @voice = voice
16
+ end
17
+
18
+ class << self
19
+ # @param data [Hash] The raw message hash.
20
+ # @return [Audio]
21
+ def deserialize(data)
22
+ new(voice: data.dig("audio", "voice"), **media_attributes(data, "audio"), **common_attributes(data))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end