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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +54 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +643 -0
- data/Rakefile +12 -0
- data/lib/ruby/whatsapp/client.rb +82 -0
- data/lib/ruby/whatsapp/configuration.rb +79 -0
- data/lib/ruby/whatsapp/instrumentation.rb +34 -0
- data/lib/ruby/whatsapp/media.rb +134 -0
- data/lib/ruby/whatsapp/message_templates/button/base.rb +26 -0
- data/lib/ruby/whatsapp/message_templates/button/copy_code.rb +59 -0
- data/lib/ruby/whatsapp/message_templates/button/otp/supported_app.rb +44 -0
- data/lib/ruby/whatsapp/message_templates/button/otp.rb +138 -0
- data/lib/ruby/whatsapp/message_templates/button/phone_number.rb +45 -0
- data/lib/ruby/whatsapp/message_templates/button/quick_reply.rb +38 -0
- data/lib/ruby/whatsapp/message_templates/button/url.rb +110 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +56 -0
- data/lib/ruby/whatsapp/message_templates/categories.rb +47 -0
- data/lib/ruby/whatsapp/message_templates/component/base.rb +112 -0
- data/lib/ruby/whatsapp/message_templates/component/body.rb +139 -0
- data/lib/ruby/whatsapp/message_templates/component/buttons.rb +95 -0
- data/lib/ruby/whatsapp/message_templates/component/carousel/card.rb +100 -0
- data/lib/ruby/whatsapp/message_templates/component/carousel.rb +71 -0
- data/lib/ruby/whatsapp/message_templates/component/footer.rb +99 -0
- data/lib/ruby/whatsapp/message_templates/component/header.rb +188 -0
- data/lib/ruby/whatsapp/message_templates/component/limited_time_offer.rb +57 -0
- data/lib/ruby/whatsapp/message_templates/component.rb +62 -0
- data/lib/ruby/whatsapp/message_templates/component_set.rb +221 -0
- data/lib/ruby/whatsapp/message_templates/example.rb +148 -0
- data/lib/ruby/whatsapp/message_templates/library_template/body_inputs.rb +81 -0
- data/lib/ruby/whatsapp/message_templates/library_template/button_inputs.rb +146 -0
- data/lib/ruby/whatsapp/message_templates/library_template.rb +114 -0
- data/lib/ruby/whatsapp/message_templates/parameter_formats.rb +49 -0
- data/lib/ruby/whatsapp/message_templates/placeholders.rb +86 -0
- data/lib/ruby/whatsapp/message_templates/response/collection.rb +74 -0
- data/lib/ruby/whatsapp/message_templates/response/created.rb +65 -0
- data/lib/ruby/whatsapp/message_templates/response/node.rb +152 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +39 -0
- data/lib/ruby/whatsapp/message_templates/response/quality_score.rb +49 -0
- data/lib/ruby/whatsapp/message_templates/response/summary.rb +68 -0
- data/lib/ruby/whatsapp/message_templates/response.rb +22 -0
- data/lib/ruby/whatsapp/message_templates/statuses.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/template.rb +180 -0
- data/lib/ruby/whatsapp/message_templates/value_object.rb +62 -0
- data/lib/ruby/whatsapp/message_templates.rb +283 -0
- data/lib/ruby/whatsapp/messages/address.rb +103 -0
- data/lib/ruby/whatsapp/messages/audio.rb +64 -0
- data/lib/ruby/whatsapp/messages/base.rb +46 -0
- data/lib/ruby/whatsapp/messages/contacts/address.rb +84 -0
- data/lib/ruby/whatsapp/messages/contacts/contact.rb +78 -0
- data/lib/ruby/whatsapp/messages/contacts/email.rb +49 -0
- data/lib/ruby/whatsapp/messages/contacts/name.rb +68 -0
- data/lib/ruby/whatsapp/messages/contacts/org.rb +41 -0
- data/lib/ruby/whatsapp/messages/contacts/phone.rb +59 -0
- data/lib/ruby/whatsapp/messages/contacts/url.rb +51 -0
- data/lib/ruby/whatsapp/messages/contacts.rb +37 -0
- data/lib/ruby/whatsapp/messages/document.rb +79 -0
- data/lib/ruby/whatsapp/messages/image.rb +72 -0
- data/lib/ruby/whatsapp/messages/interactive/base.rb +15 -0
- data/lib/ruby/whatsapp/messages/interactive/body.rb +39 -0
- data/lib/ruby/whatsapp/messages/interactive/footer.rb +40 -0
- data/lib/ruby/whatsapp/messages/interactive/header.rb +102 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section/row.rb +66 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section.rb +64 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons.rb +63 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button/quick_reply.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button.rb +47 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card.rb +78 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/product_carousel/card.rb +55 -0
- data/lib/ruby/whatsapp/messages/interactive/product_carousel.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/reply_buttons/button.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive/reply_buttons.rb +47 -0
- data/lib/ruby/whatsapp/messages/interactive/url_button.rb +68 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +112 -0
- data/lib/ruby/whatsapp/messages/location.rb +67 -0
- data/lib/ruby/whatsapp/messages/location_request.rb +41 -0
- data/lib/ruby/whatsapp/messages/mark_message_as_read.rb +42 -0
- data/lib/ruby/whatsapp/messages/reaction.rb +61 -0
- data/lib/ruby/whatsapp/messages/response/contacts.rb +36 -0
- data/lib/ruby/whatsapp/messages/response/messages.rb +27 -0
- data/lib/ruby/whatsapp/messages/response.rb +49 -0
- data/lib/ruby/whatsapp/messages/sticker.rb +62 -0
- data/lib/ruby/whatsapp/messages/template/component.rb +67 -0
- data/lib/ruby/whatsapp/messages/template/language.rb +55 -0
- data/lib/ruby/whatsapp/messages/template/parameter.rb +113 -0
- data/lib/ruby/whatsapp/messages/template.rb +62 -0
- data/lib/ruby/whatsapp/messages/text.rb +43 -0
- data/lib/ruby/whatsapp/messages/video.rb +68 -0
- data/lib/ruby/whatsapp/messages.rb +116 -0
- data/lib/ruby/whatsapp/railtie.rb +14 -0
- data/lib/ruby/whatsapp/request_error.rb +5 -0
- data/lib/ruby/whatsapp/response_handling.rb +44 -0
- data/lib/ruby/whatsapp/utils/language_codes.rb +96 -0
- data/lib/ruby/whatsapp/version.rb +5 -0
- data/lib/ruby/whatsapp/webhook/account_alerts.rb +58 -0
- data/lib/ruby/whatsapp/webhook/account_review_update.rb +27 -0
- data/lib/ruby/whatsapp/webhook/account_update/ban_info.rb +33 -0
- data/lib/ruby/whatsapp/webhook/account_update.rb +46 -0
- data/lib/ruby/whatsapp/webhook/automatic_events.rb +43 -0
- data/lib/ruby/whatsapp/webhook/business_capability_update.rb +36 -0
- data/lib/ruby/whatsapp/webhook/change.rb +61 -0
- data/lib/ruby/whatsapp/webhook/contact.rb +36 -0
- data/lib/ruby/whatsapp/webhook/entry.rb +35 -0
- data/lib/ruby/whatsapp/webhook/error.rb +47 -0
- data/lib/ruby/whatsapp/webhook/history.rb +54 -0
- data/lib/ruby/whatsapp/webhook/installer.rb +61 -0
- data/lib/ruby/whatsapp/webhook/message/audio.rb +28 -0
- data/lib/ruby/whatsapp/webhook/message/base.rb +61 -0
- data/lib/ruby/whatsapp/webhook/message/button.rb +33 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/address.rb +68 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +68 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/email.rb +35 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/name.rb +62 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/org.rb +40 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/phone.rb +40 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/url.rb +35 -0
- data/lib/ruby/whatsapp/webhook/message/contacts.rb +31 -0
- data/lib/ruby/whatsapp/webhook/message/context.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/document.rb +32 -0
- data/lib/ruby/whatsapp/webhook/message/image.rb +18 -0
- data/lib/ruby/whatsapp/webhook/message/interactive.rb +53 -0
- data/lib/ruby/whatsapp/webhook/message/location.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/media.rb +54 -0
- data/lib/ruby/whatsapp/webhook/message/order/product_item.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/order.rb +43 -0
- data/lib/ruby/whatsapp/webhook/message/reaction.rb +37 -0
- data/lib/ruby/whatsapp/webhook/message/referral.rb +80 -0
- data/lib/ruby/whatsapp/webhook/message/sticker.rb +32 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/text.rb +28 -0
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +39 -0
- data/lib/ruby/whatsapp/webhook/message/video.rb +18 -0
- data/lib/ruby/whatsapp/webhook/message.rb +39 -0
- data/lib/ruby/whatsapp/webhook/message_template_components_update.rb +53 -0
- data/lib/ruby/whatsapp/webhook/message_template_quality_update.rb +59 -0
- data/lib/ruby/whatsapp/webhook/message_template_status_update.rb +54 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +55 -0
- data/lib/ruby/whatsapp/webhook/metadata.rb +35 -0
- data/lib/ruby/whatsapp/webhook/notification.rb +36 -0
- data/lib/ruby/whatsapp/webhook/partner_solutions.rb +36 -0
- data/lib/ruby/whatsapp/webhook/payment_configuration_update.rb +51 -0
- data/lib/ruby/whatsapp/webhook/phone_number_name_update.rb +51 -0
- data/lib/ruby/whatsapp/webhook/phone_number_quality_update.rb +45 -0
- data/lib/ruby/whatsapp/webhook/security.rb +41 -0
- data/lib/ruby/whatsapp/webhook/signature.rb +28 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +38 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +48 -0
- data/lib/ruby/whatsapp/webhook/status/conversation.rb +42 -0
- data/lib/ruby/whatsapp/webhook/status/pricing.rb +38 -0
- data/lib/ruby/whatsapp/webhook/status.rb +65 -0
- data/lib/ruby/whatsapp/webhook/template_category_update.rb +64 -0
- data/lib/ruby/whatsapp/webhook/templates/webhooks_controller.rb.tt +31 -0
- data/lib/ruby/whatsapp/webhook/unknown_field.rb +37 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +47 -0
- data/lib/ruby/whatsapp/webhook/verification.rb +27 -0
- data/lib/ruby/whatsapp/webhook.rb +8 -0
- data/lib/ruby/whatsapp.rb +49 -0
- data/lib/tasks/whatsapp.rake +10 -0
- data/sig/ruby/whatsapp.rbs +6 -0
- 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,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
|