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,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,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
|