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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ # Sticker messages display animated or static sticker images in a WhatsApp message.
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/sticker-messages
7
+ class Sticker < Base
8
+ module Defaults
9
+ TYPE = "sticker"
10
+ end
11
+
12
+ # @!attribute [rw] id
13
+ # @return [String]
14
+ attr_accessor :id
15
+
16
+ # @!attribute [rw] link
17
+ # @return [String]
18
+ attr_accessor :link
19
+
20
+ validate :validate_id_or_link
21
+
22
+ # @param id [String, nil] ID of the uploaded media asset.
23
+ # @param link [String, nil] URL of a publicly hosted media asset. Prefer uploading via
24
+ # Media#upload and passing +id+ for better performance.
25
+ # @param kwargs [Hash] Additional keyword arguments.
26
+ # @raise [ActiveModel::ValidationError] if validation fails.
27
+ def initialize(id: nil, link: nil, **)
28
+ super(**)
29
+
30
+ @id = id
31
+ @link = link
32
+
33
+ validate!
34
+ end
35
+
36
+ # Serializes the sticker message to a hash format suitable for the WhatsApp API.
37
+ # @return [Hash] The serialized sticker message.
38
+ def serialize
39
+ envelope(type: Defaults::TYPE, sticker:)
40
+ end
41
+
42
+ private
43
+
44
+ # Serializes the sticker information to a hash format.
45
+ # @return [Hash] The serialized sticker information.
46
+ def sticker
47
+ {
48
+ id:,
49
+ link:,
50
+ }.compact
51
+ end
52
+
53
+ # Validates that either id or link is present.
54
+ # @return [void]
55
+ def validate_id_or_link
56
+ return if id.present? || link.present?
57
+
58
+ errors.add(:base, "Either id or link must be present")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ class Template
6
+ # Component for template messages (header, body, button)
7
+ class Component
8
+ include ActiveModel::Validations
9
+
10
+ module Types
11
+ HEADER = "header"
12
+ BODY = "body"
13
+ BUTTON = "button"
14
+ end
15
+
16
+ module SubTypes
17
+ # Button sub_types
18
+ QUICK_REPLY = "quick_reply"
19
+ URL = "url"
20
+ COPY_CODE = "copy_code"
21
+ end
22
+
23
+ # @!attribute [rw] type
24
+ # @return [String]
25
+ attr_accessor :type
26
+
27
+ # @!attribute [rw] sub_type
28
+ # @return [String, nil]
29
+ attr_accessor :sub_type
30
+
31
+ # @!attribute [rw] parameters
32
+ # @return [Array<Parameter>]
33
+ attr_accessor :parameters
34
+
35
+ # @!attribute [rw] index
36
+ # @return [Integer, nil]
37
+ attr_accessor :index
38
+
39
+ validates :type, presence: true, inclusion: { in: Types.constants.map { |c| Types.const_get(c) } }
40
+
41
+ # @param type [String] The type of component (header, body, button).
42
+ # @param sub_type [String, nil] The sub-type for button components (quick_reply, url).
43
+ # @param parameters [Array<Hash, Parameter>] Array of parameter hashes.
44
+ # @param index [Integer, nil] Button index for multiple buttons.
45
+ def initialize(type:, sub_type: nil, parameters: [], index: nil)
46
+ @type = type
47
+ @sub_type = sub_type
48
+ @parameters = parameters.map { |param| param.is_a?(Parameter) ? param : Parameter.new(**param) }
49
+ @index = index
50
+
51
+ validate!
52
+ end
53
+
54
+ # Serializes the component to a hash format suitable for the WhatsApp API.
55
+ # @return [Hash] The serialized component.
56
+ def serialize
57
+ result = { type: }
58
+ result[:sub_type] = sub_type if sub_type
59
+ result[:index] = index if index
60
+ result[:parameters] = parameters.map(&:serialize) if parameters.any?
61
+
62
+ result.compact
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ class Template
6
+ # Language code for template messages
7
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages
8
+ class Language
9
+ include ActiveModel::Validations
10
+
11
+ # @!attribute [rw] code
12
+ # @return [String]
13
+ attr_accessor :code
14
+
15
+ validate :validate_language_code
16
+
17
+ # @param code [String] The language code (e.g., "en_US", "pt_BR").
18
+ # @raise [ActiveModel::ValidationError] if validation fails.
19
+ def initialize(code:)
20
+ @code = code
21
+
22
+ validate!
23
+ end
24
+
25
+ # Serializes the language to a hash format suitable for the WhatsApp API.
26
+ # @return [Hash] The serialized language.
27
+ def serialize
28
+ {
29
+ code:,
30
+ }
31
+ end
32
+
33
+ class << self
34
+ # Serializes the language to a hash format suitable for the WhatsApp API.
35
+ # @param code [String] The language code.
36
+ # @return [Hash] The serialized language.
37
+ def serialize(code:)
38
+ new(code:).serialize
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Validates that the language code is supported by WhatsApp
45
+ # @return [void]
46
+ def validate_language_code
47
+ return if Utils::LanguageCodes.valid?(code)
48
+
49
+ errors.add(:code,
50
+ "is not a valid WhatsApp language code. Supported codes: #{Utils::LanguageCodes.all.join(', ')}")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ class Template
6
+ # Parameters for template components
7
+ class Parameter
8
+ include ActiveModel::Validations
9
+
10
+ module Types
11
+ TEXT = "text"
12
+ CURRENCY = "currency"
13
+ DATE_TIME = "date_time"
14
+ IMAGE = "image"
15
+ DOCUMENT = "document"
16
+ VIDEO = "video"
17
+ LOCATION = "location"
18
+ PAYLOAD = "payload"
19
+ end
20
+
21
+ # @!attribute [rw] type
22
+ # @return [String]
23
+ attr_accessor :type
24
+
25
+ # @!attribute [rw] text
26
+ # @return [String, nil]
27
+ attr_accessor :text
28
+
29
+ # @!attribute [rw] currency
30
+ # @return [Hash, nil]
31
+ attr_accessor :currency
32
+
33
+ # @!attribute [rw] date_time
34
+ # @return [Hash, nil]
35
+ attr_accessor :date_time
36
+
37
+ # @!attribute [rw] image
38
+ # @return [Hash, nil]
39
+ attr_accessor :image
40
+
41
+ # @!attribute [rw] document
42
+ # @return [Hash, nil]
43
+ attr_accessor :document
44
+
45
+ # @!attribute [rw] video
46
+ # @return [Hash, nil]
47
+ attr_accessor :video
48
+
49
+ # @!attribute [rw] location
50
+ # @return [Hash, nil]
51
+ attr_accessor :location
52
+
53
+ # @!attribute [rw] payload
54
+ # @return [String, nil]
55
+ attr_accessor :payload
56
+
57
+ validates :type, presence: true, inclusion: { in: Types.constants.map { |c| Types.const_get(c) } }
58
+
59
+ # @param type [String] The parameter type (one of Types: text, currency, date_time,
60
+ # image, document, video, location, payload).
61
+ # @param text [String, nil] The text value for text parameters.
62
+ # @param currency [Hash, nil] Currency details for currency parameters.
63
+ # @param date_time [Hash, nil] Date/time details for date_time parameters.
64
+ # @param image [Hash, nil] Image details for image parameters.
65
+ # @param document [Hash, nil] Document details for document parameters.
66
+ # @param video [Hash, nil] Video details for video parameters.
67
+ # @param location [Hash, nil] Location details for location parameters.
68
+ # @param payload [String, nil] Payload for button parameters.
69
+ def initialize(type:, text: nil, currency: nil, date_time: nil, image: nil, document: nil, video: nil,
70
+ location: nil, payload: nil)
71
+ @type = type
72
+ @text = text
73
+ @currency = currency
74
+ @date_time = date_time
75
+ @image = image
76
+ @document = document
77
+ @video = video
78
+ @location = location
79
+ @payload = payload
80
+
81
+ validate!
82
+ end
83
+
84
+ # Serializes the parameter to a hash format suitable for the WhatsApp API.
85
+ # @return [Hash] The serialized parameter.
86
+ def serialize
87
+ result = { type: }
88
+
89
+ case type
90
+ when Types::TEXT
91
+ result[:text] = text
92
+ when Types::CURRENCY
93
+ result[:currency] = currency
94
+ when Types::DATE_TIME
95
+ result[:date_time] = date_time
96
+ when Types::IMAGE
97
+ result[:image] = image
98
+ when Types::DOCUMENT
99
+ result[:document] = document
100
+ when Types::VIDEO
101
+ result[:video] = video
102
+ when Types::LOCATION
103
+ result[:location] = location
104
+ when Types::PAYLOAD
105
+ result[:payload] = payload
106
+ end
107
+
108
+ result.compact
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ # Template messages allow you to send marketing, utility, and authentication templates to WhatsApp users.
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/template-messages
7
+ class Template < Base
8
+ module Defaults
9
+ TYPE = "template"
10
+ end
11
+
12
+ # @!attribute [rw] name
13
+ # @return [String]
14
+ attr_accessor :name
15
+
16
+ # @!attribute [rw] language
17
+ # @return [Language]
18
+ attr_accessor :language
19
+
20
+ # @!attribute [rw] components
21
+ # @return [Array<Component>]
22
+ attr_accessor :components
23
+
24
+ validates :name, presence: true
25
+ validates :language, presence: true
26
+
27
+ # @param name [String] The template name.
28
+ # @param language [Hash, Language] The language configuration with code.
29
+ # @param components [Array<Hash, Component>] Array of component hashes (optional).
30
+ # @param kwargs [Hash] Additional keyword arguments.
31
+ # @raise [ActiveModel::ValidationError] if validation fails.
32
+ def initialize(name:, language:, components: [], **)
33
+ super(**)
34
+
35
+ @name = name
36
+ @language = language.is_a?(Language) ? language : Language.new(**language)
37
+ @components = components.map { |comp| comp.is_a?(Component) ? comp : Component.new(**comp) }
38
+
39
+ validate!
40
+ end
41
+
42
+ # Serializes the template message to a hash format suitable for the WhatsApp API.
43
+ # @return [Hash] The serialized template message.
44
+ def serialize
45
+ envelope(type: Defaults::TYPE, template: template_payload)
46
+ end
47
+
48
+ private
49
+
50
+ # @return [Hash] The serialized template payload.
51
+ def template_payload
52
+ {
53
+ name:,
54
+ language: language.serialize,
55
+ components: components.map(&:serialize),
56
+ }.tap do |payload|
57
+ payload.delete(:components) if components.empty?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ # Text messages are messages containing only a text body and an optional link preview.
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/text-messages
7
+ class Text < Base
8
+ module Defaults
9
+ TYPE = "text"
10
+ end
11
+
12
+ # @!attribute [rw] body
13
+ # @return [String]
14
+ attr_accessor :body
15
+
16
+ # @!attribute [rw] preview_url
17
+ # @return [Boolean]
18
+ attr_accessor :preview_url
19
+
20
+ validates :body, presence: true, length: { maximum: 4096 }
21
+ validates :preview_url, inclusion: { in: [true, false] }
22
+
23
+ # @param body [String] The text of the message.
24
+ # @param preview_url [Boolean] Whether to enable URL preview in the message.
25
+ # @param kwargs [Hash] Additional keyword arguments.
26
+ # @raise [ActiveModel::ValidationError] if validation fails.
27
+ def initialize(body:, preview_url: true, **)
28
+ super(**)
29
+
30
+ @body = body
31
+ @preview_url = preview_url
32
+
33
+ validate!
34
+ end
35
+
36
+ # Serializes the text message to a hash format suitable for the WhatsApp API.
37
+ # @return [Hash] The serialized text message.
38
+ def serialize
39
+ envelope(type: Defaults::TYPE, text: { body:, preview_url: })
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ # Video messages display a thumbnail preview of a video with an optional caption.
6
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/video-messages
7
+ class Video < Base
8
+ module Defaults
9
+ TYPE = "video"
10
+ end
11
+
12
+ # @!attribute [rw] id
13
+ # @return [String, nil]
14
+ attr_accessor :id
15
+
16
+ # @!attribute [rw] link
17
+ # @return [String, nil]
18
+ attr_accessor :link
19
+
20
+ # @!attribute [rw] caption
21
+ # @return [String, nil]
22
+ attr_accessor :caption
23
+
24
+ validate :validate_id_or_link
25
+ validates :caption, length: { maximum: 1024 }, allow_nil: true
26
+
27
+ # @param id [String, nil] ID of the uploaded media asset.
28
+ # @param link [String, nil] URL of the media asset hosted on your public server.
29
+ # @param caption [String, nil] Video caption text (maximum 1024 characters).
30
+ # @param kwargs [Hash] Additional keyword arguments.
31
+ # @raise [ActiveModel::ValidationError] if validation fails.
32
+ def initialize(id: nil, link: nil, caption: nil, **)
33
+ super(**)
34
+
35
+ @id = id
36
+ @link = link
37
+ @caption = caption
38
+
39
+ validate!
40
+ end
41
+
42
+ # Serializes the video message to a hash format suitable for the WhatsApp API.
43
+ # @return [Hash] The serialized video message.
44
+ def serialize
45
+ envelope(type: Defaults::TYPE, video: video_payload)
46
+ end
47
+
48
+ private
49
+
50
+ # @return [Hash] The serialized video payload.
51
+ def video_payload
52
+ {
53
+ id:,
54
+ link:,
55
+ caption:,
56
+ }.compact
57
+ end
58
+
59
+ # Validates that either id or link is present
60
+ # @return [void]
61
+ def validate_id_or_link
62
+ return if id.present? || link.present?
63
+
64
+ errors.add(:base, "Either id or link must be present")
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class Messages
5
+ include ResponseHandling
6
+ extend ResponseHandling
7
+
8
+ # Maps a public message kind to its implementing class. Resolution goes
9
+ # through this frozen whitelist (never `const_get` on caller input), so an
10
+ # unknown or hostile `kind` can only raise, never resolve an arbitrary
11
+ # constant.
12
+ KINDS = {
13
+ text: Text,
14
+ image: Image,
15
+ audio: Audio,
16
+ video: Video,
17
+ document: Document,
18
+ sticker: Sticker,
19
+ contacts: Contacts,
20
+ reaction: Reaction,
21
+ location: Location,
22
+ address: Address,
23
+ location_request: LocationRequest,
24
+ template: Template,
25
+ interactive: Interactive,
26
+ }.freeze
27
+
28
+ class << self
29
+ # Defines a `send_<kind>!` convenience class method for every kind in
30
+ # KINDS (e.g. `send_text!`, `send_template!`), equivalent to
31
+ # `new(kind:, payload:, client:).send!`. Adding a kind to KINDS above
32
+ # automatically gets one of these for free.
33
+ KINDS.each_key do |kind|
34
+ define_method(:"send_#{kind}!") do |client: Client.new, **payload|
35
+ new(kind:, payload:, client:).send!
36
+ end
37
+ end
38
+
39
+ # Marks a previously received message as read. Deliberately not part of
40
+ # KINDS: this action has no recipient/type envelope, so it can't be
41
+ # dispatched through the kind:/payload: factory the way the rest of the
42
+ # message kinds are.
43
+ # @param message_id [String] The WhatsApp message ID (WAMID) to mark as read.
44
+ # @param client [Whatsapp::Client] The WhatsApp client instance
45
+ # @return [Whatsapp::Messages::Response]
46
+ # @raise [ActiveModel::ValidationError] if message_id is missing
47
+ # @raise [Whatsapp::RequestError] if the request fails
48
+ def mark_message_as_read!(message_id:, client: Client.new)
49
+ payload = MarkMessageAsRead.new(message_id:)
50
+
51
+ response = client.connection.post(
52
+ client.path_for(client.phone_id, "messages"),
53
+ json: payload.serialize
54
+ )
55
+
56
+ checked_response = handle_response!(response, error_class: RequestError, action: "mark message as read")
57
+ Response.deserialize(parse_json(checked_response))
58
+ end
59
+ end
60
+
61
+ class PayloadError < Whatsapp::Error; end
62
+
63
+ # @!attribute [rw] payload
64
+ # @return [Whatsapp::Messages::Base]
65
+ attr_accessor :payload
66
+
67
+ # @!attribute [rw] client
68
+ # @return [Whatsapp::Client]
69
+ attr_accessor :client
70
+
71
+ # @param kind [Symbol, String] The type of message (e.g., :text, :template)
72
+ # @param payload [Hash] The message payload passed to the message class
73
+ # @param client [Whatsapp::Client] The WhatsApp client instance
74
+ # @raise [PayloadError] if the kind is unknown
75
+ def initialize(kind:, payload:, client: Client.new)
76
+ klass = KINDS.fetch(kind.to_sym) do
77
+ raise PayloadError, "Unknown message kind: #{kind.inspect}. Known kinds: #{KINDS.keys.join(', ')}"
78
+ end
79
+
80
+ @payload = klass.new(**payload)
81
+ @client = client
82
+ end
83
+
84
+ # Sends the message to the WhatsApp API.
85
+ # @return [Whatsapp::Messages::Response] The response from the API
86
+ # @raise [Whatsapp::Messages::PayloadError] if the payload is invalid
87
+ # @raise [Whatsapp::RequestError] if the request fails
88
+ def send!
89
+ raise PayloadError, "Invalid message: #{payload.errors.full_messages.join(', ')}" unless payload.valid?
90
+
91
+ response = perform_request!
92
+ parse!(response)
93
+ end
94
+
95
+ private
96
+
97
+ # Performs the HTTP request to send the message.
98
+ # @return [HTTP::Response] The successful HTTP response from the API
99
+ # @raise [Whatsapp::RequestError] if the request fails
100
+ def perform_request!
101
+ response = client.connection.post(
102
+ client.path_for(client.phone_id, "messages"),
103
+ json: payload.serialize
104
+ )
105
+
106
+ handle_response!(response, error_class: RequestError, action: "send message")
107
+ end
108
+
109
+ # Parses the HTTP response into a Whatsapp::Messages::Response object.
110
+ # @param response [HTTP::Response] The HTTP response from the API
111
+ # @return [Whatsapp::Messages::Response] The parsed response object
112
+ def parse!(response)
113
+ Response.deserialize(parse_json(response))
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ # Registers `rake whatsapp:install:webhook` with the host Rails app. Only
5
+ # defined when Rails is already loaded — this gem has no Rails dependency of
6
+ # its own. Deliberately excluded from Zeitwerk (see `lib/ruby/whatsapp.rb`)
7
+ # and required eagerly instead: Rails discovers Railtie subclasses at boot by
8
+ # class definition, so autoloading it lazily would be too late.
9
+ class Railtie < Rails::Railtie
10
+ rake_tasks do
11
+ load File.expand_path("../../tasks/whatsapp.rake", __dir__)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ class RequestError < Whatsapp::Error; end
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ # Shared handling of HTTP responses for API operations.
5
+ module ResponseHandling
6
+ # Maximum number of response-body characters to include in error messages,
7
+ # to avoid leaking large or sensitive payloads into logs.
8
+ MAX_ERROR_BODY = 500
9
+
10
+ private
11
+
12
+ # Returns the response when successful, otherwise raises error_class with a
13
+ # message describing the failed action and a truncated body.
14
+ # @param response [HTTP::Response]
15
+ # @param error_class [Class] the exception class to raise on failure
16
+ # @param action [String] a short description of the attempted action
17
+ # @return [HTTP::Response] the successful response
18
+ def handle_response!(response, error_class:, action:)
19
+ return response if response.status.success?
20
+
21
+ raise error_class, "Failed to #{action}: #{response.status} - #{truncate_body(response)}"
22
+ end
23
+
24
+ # Parses a response body as JSON, naming the format instead of inferring it.
25
+ #
26
+ # The Graph API labels its JSON responses `text/javascript`, which the http gem
27
+ # has no MIME handler for — `HTTP::Response#parse` with no argument raises
28
+ # `HTTP::Error: Unknown MIME type: text/javascript`. Every endpoint this gem
29
+ # talks to returns JSON, so the format is always safe to state outright.
30
+ # @param response [HTTP::Response]
31
+ # @return [Hash, Array] the parsed body
32
+ def parse_json(response)
33
+ response.parse(:json)
34
+ end
35
+
36
+ # @return [String] the response body truncated to MAX_ERROR_BODY characters
37
+ def truncate_body(response)
38
+ body = response.body.to_s
39
+ return body if body.length <= MAX_ERROR_BODY
40
+
41
+ "#{body[0, MAX_ERROR_BODY]}… (truncated)"
42
+ end
43
+ end
44
+ end