ruby-whatsapp 0.3.1 → 0.4.2
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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +292 -497
- data/docs/README.md +73 -0
- data/docs/business_phone_number/README.md +232 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/configuration.md +172 -0
- data/docs/errors.md +208 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +179 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +135 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +281 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +166 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +209 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/history.md +44 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +385 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +53 -0
- data/docs/webhooks/smb_message_echoes.md +54 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_preferences.md +48 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/deregister.rb +38 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/register.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/request_code.rb +85 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +39 -0
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number/verify_code.rb +63 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +29 -0
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/list.rb +28 -0
- data/lib/ruby/whatsapp/subscribed_app/response/app.rb +57 -0
- data/lib/ruby/whatsapp/subscribed_app/response/collection.rb +41 -0
- data/lib/ruby/whatsapp/subscribed_app/response/subscription.rb +49 -0
- data/lib/ruby/whatsapp/subscribed_app/response/unsubscription.rb +31 -0
- data/lib/ruby/whatsapp/subscribed_app/response.rb +19 -0
- data/lib/ruby/whatsapp/subscribed_app/subscribe.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +24 -0
- data/lib/ruby/whatsapp/subscribed_app/unsubscribe.rb +26 -0
- data/lib/ruby/whatsapp/subscribed_app.rb +15 -0
- data/lib/ruby/whatsapp/version.rb +1 -1
- metadata +80 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Profile
|
|
6
|
+
# Updates a business profile: its about text, description, contact details, websites,
|
|
7
|
+
# industry vertical, and profile picture.
|
|
8
|
+
#
|
|
9
|
+
# A validated instance rather than a bare class method (unlike {Get}, which has nothing
|
|
10
|
+
# to check): Meta documents a closed vertical enum and per-field character limits, and
|
|
11
|
+
# since every field is optional an all-nil call would spend a request to change nothing.
|
|
12
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
|
|
13
|
+
class Update
|
|
14
|
+
include ActiveModel::Validations
|
|
15
|
+
extend ResponseHandling
|
|
16
|
+
extend Transport
|
|
17
|
+
|
|
18
|
+
module Defaults
|
|
19
|
+
MESSAGING_PRODUCT = "whatsapp"
|
|
20
|
+
EDGE = "whatsapp_business_profile"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The character and count limits Meta documents for a business profile.
|
|
24
|
+
module Limits
|
|
25
|
+
ABOUT = 139
|
|
26
|
+
ADDRESS = 256
|
|
27
|
+
DESCRIPTION = 512
|
|
28
|
+
EMAIL = 128
|
|
29
|
+
WEBSITES = 2
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Every updatable field, in payload order. Drives both the "change something"
|
|
33
|
+
# guard and its message, so the two cannot drift apart.
|
|
34
|
+
ATTRIBUTES = %i[about address description email vertical websites profile_picture_handle].freeze
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] about
|
|
37
|
+
# @return [String, nil] The profile's about text, at most {Limits::ABOUT} characters.
|
|
38
|
+
attr_accessor :about
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] address
|
|
41
|
+
# @return [String, nil] The business's address, at most {Limits::ADDRESS} characters.
|
|
42
|
+
attr_accessor :address
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] description
|
|
45
|
+
# @return [String, nil] The business description, at most {Limits::DESCRIPTION} characters.
|
|
46
|
+
attr_accessor :description
|
|
47
|
+
|
|
48
|
+
# @!attribute [rw] email
|
|
49
|
+
# @return [String, nil] The contact email, at most {Limits::EMAIL} characters.
|
|
50
|
+
attr_accessor :email
|
|
51
|
+
|
|
52
|
+
# @!attribute [rw] vertical
|
|
53
|
+
# @return [String, nil] One of {Verticals::ALL}.
|
|
54
|
+
attr_accessor :vertical
|
|
55
|
+
|
|
56
|
+
# @!attribute [rw] websites
|
|
57
|
+
# @return [Array<String>, nil] At most {Limits::WEBSITES} URLs.
|
|
58
|
+
attr_accessor :websites
|
|
59
|
+
|
|
60
|
+
# @!attribute [rw] profile_picture_handle
|
|
61
|
+
# @return [String, nil] A picture handle from Meta's Resumable Upload API.
|
|
62
|
+
attr_accessor :profile_picture_handle
|
|
63
|
+
|
|
64
|
+
validate :validate_any_attribute_present
|
|
65
|
+
validate :validate_websites_limit
|
|
66
|
+
validates :about, length: { maximum: Limits::ABOUT }, allow_nil: true
|
|
67
|
+
validates :address, length: { maximum: Limits::ADDRESS }, allow_nil: true
|
|
68
|
+
validates :description, length: { maximum: Limits::DESCRIPTION }, allow_nil: true
|
|
69
|
+
validates :email, length: { maximum: Limits::EMAIL }, allow_nil: true
|
|
70
|
+
validates :vertical, inclusion: { in: Verticals::ALL }, allow_nil: true
|
|
71
|
+
validates :profile_picture_handle, presence: true, allow_nil: true
|
|
72
|
+
|
|
73
|
+
# @param about [String, nil] See {#about}.
|
|
74
|
+
# @param address [String, nil] See {#address}.
|
|
75
|
+
# @param description [String, nil] See {#description}.
|
|
76
|
+
# @param email [String, nil] Length only — Meta validates the address itself
|
|
77
|
+
# server-side, and a format check here would reject valid exotic addresses.
|
|
78
|
+
# @param vertical [String, Symbol, nil] One of {Verticals::ALL}, either casing.
|
|
79
|
+
# @param websites [Array<String>, nil] At most {Limits::WEBSITES} URLs. An empty
|
|
80
|
+
# array is a deliberate instruction to clear the list, not an omission.
|
|
81
|
+
# @param profile_picture_handle [String, nil] Opaque to this gem; presence only.
|
|
82
|
+
# @raise [ActiveModel::ValidationError] if every field is nil, or one breaks a
|
|
83
|
+
# documented limit.
|
|
84
|
+
def initialize(about: nil, address: nil, description: nil, email: nil, vertical: nil,
|
|
85
|
+
websites: nil, profile_picture_handle: nil)
|
|
86
|
+
@about = about
|
|
87
|
+
@address = address
|
|
88
|
+
@description = description
|
|
89
|
+
@email = email
|
|
90
|
+
@vertical = Verticals.normalize(vertical)
|
|
91
|
+
@websites = websites
|
|
92
|
+
@profile_picture_handle = profile_picture_handle
|
|
93
|
+
|
|
94
|
+
validate!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Hash] The update payload. `messaging_product` is documented required so it
|
|
98
|
+
# is always sent; every other field is optional and an omitted one is compacted away
|
|
99
|
+
# rather than sent as null, which Meta would read as an instruction to blank it.
|
|
100
|
+
# An empty `websites` array survives compaction on purpose — that *is* the clear.
|
|
101
|
+
def serialize
|
|
102
|
+
{ messaging_product: Defaults::MESSAGING_PRODUCT, about:, address:, description:,
|
|
103
|
+
email:, vertical:, websites:, profile_picture_handle:, }.compact
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class << self
|
|
107
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
108
|
+
# @param about [String, nil] See {#initialize}.
|
|
109
|
+
# @param address [String, nil] See {#initialize}.
|
|
110
|
+
# @param description [String, nil] See {#initialize}.
|
|
111
|
+
# @param email [String, nil] See {#initialize}.
|
|
112
|
+
# @param vertical [String, Symbol, nil] See {#initialize}.
|
|
113
|
+
# @param websites [Array<String>, nil] See {#initialize}.
|
|
114
|
+
# @param profile_picture_handle [String, nil] See {#initialize}.
|
|
115
|
+
# @return [BusinessPhoneNumber::Response]
|
|
116
|
+
# @raise [ActiveModel::ValidationError] if no field is given, or one is invalid.
|
|
117
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
118
|
+
def call(client: Client.new, about: nil, address: nil, description: nil, email: nil,
|
|
119
|
+
vertical: nil, websites: nil, profile_picture_handle: nil)
|
|
120
|
+
body = new(about:, address:, description:, email:, vertical:, websites:,
|
|
121
|
+
profile_picture_handle:).serialize
|
|
122
|
+
response = client.connection.post(edge_path(client, Defaults::EDGE), json: body)
|
|
123
|
+
|
|
124
|
+
BusinessPhoneNumber::Response.deserialize(
|
|
125
|
+
parse_json(handle_response!(response, error_class: Error, action: "update business profile"))
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# @return [void]
|
|
133
|
+
def validate_any_attribute_present
|
|
134
|
+
return if ATTRIBUTES.any? { |attribute| !public_send(attribute).nil? }
|
|
135
|
+
|
|
136
|
+
errors.add(:base, "at least one of #{ATTRIBUTES.join(', ')} must be provided")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [void]
|
|
140
|
+
def validate_websites_limit
|
|
141
|
+
return if websites.nil? || websites.size <= Limits::WEBSITES
|
|
142
|
+
|
|
143
|
+
errors.add(:base, "websites accepts at most #{Limits::WEBSITES} URLs")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Profile
|
|
6
|
+
# The industry verticals Meta publishes for a business profile.
|
|
7
|
+
#
|
|
8
|
+
# Its own file rather than nested inside {Update}, since two classes use it: {Update}
|
|
9
|
+
# validates against it and {Details} exposes the raw value for comparison against it.
|
|
10
|
+
# That follows {Whatsapp::MessageTemplates::Categories}' precedent rather than
|
|
11
|
+
# {Register::DataLocalizationRegions}', which has a single consumer.
|
|
12
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
|
|
13
|
+
module Verticals
|
|
14
|
+
ALCOHOL = "ALCOHOL"
|
|
15
|
+
APPAREL = "APPAREL"
|
|
16
|
+
AUTO = "AUTO"
|
|
17
|
+
BEAUTY = "BEAUTY"
|
|
18
|
+
EDU = "EDU"
|
|
19
|
+
ENTERTAIN = "ENTERTAIN"
|
|
20
|
+
EVENT_PLAN = "EVENT_PLAN"
|
|
21
|
+
FINANCE = "FINANCE"
|
|
22
|
+
GOVT = "GOVT"
|
|
23
|
+
GROCERY = "GROCERY"
|
|
24
|
+
HEALTH = "HEALTH"
|
|
25
|
+
HOTEL = "HOTEL"
|
|
26
|
+
NONPROFIT = "NONPROFIT"
|
|
27
|
+
ONLINE_GAMBLING = "ONLINE_GAMBLING"
|
|
28
|
+
OTC_DRUGS = "OTC_DRUGS"
|
|
29
|
+
OTHER = "OTHER"
|
|
30
|
+
PHYSICAL_GAMBLING = "PHYSICAL_GAMBLING"
|
|
31
|
+
PROF_SERVICES = "PROF_SERVICES"
|
|
32
|
+
RESTAURANT = "RESTAURANT"
|
|
33
|
+
RETAIL = "RETAIL"
|
|
34
|
+
TRAVEL = "TRAVEL"
|
|
35
|
+
|
|
36
|
+
ALL = [ALCOHOL, APPAREL, AUTO, BEAUTY, EDU, ENTERTAIN, EVENT_PLAN, FINANCE, GOVT,
|
|
37
|
+
GROCERY, HEALTH, HOTEL, NONPROFIT, ONLINE_GAMBLING, OTC_DRUGS, OTHER,
|
|
38
|
+
PHYSICAL_GAMBLING, PROF_SERVICES, RESTAURANT, RETAIL, TRAVEL,].freeze
|
|
39
|
+
|
|
40
|
+
# Normalizes caller input to a canonical uppercase vertical.
|
|
41
|
+
# Unrecognized values are returned untouched so the inclusion validator can
|
|
42
|
+
# report them, matching {Register::DataLocalizationRegions.normalize}.
|
|
43
|
+
# @param value [String, Symbol, nil]
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
def self.normalize(value)
|
|
46
|
+
return if value.nil?
|
|
47
|
+
|
|
48
|
+
candidate = value.to_s.upcase
|
|
49
|
+
ALL.include?(candidate) ? candidate : value.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Reads and updates a business phone number's **business profile** — the public card a
|
|
6
|
+
# WhatsApp user sees before they reply: the about line, description, address, email,
|
|
7
|
+
# websites, industry vertical, and profile picture.
|
|
8
|
+
#
|
|
9
|
+
# Unlike {Account}, this is not an exception to the module's `phone_id` rule: Meta exposes
|
|
10
|
+
# the profile as an edge on the phone number itself
|
|
11
|
+
# (`GET`/`POST /{phone_id}/whatsapp_business_profile`), so {Get} and {Update} address the
|
|
12
|
+
# same ID the onboarding actions do and reuse {BusinessPhoneNumber::Transport} unchanged.
|
|
13
|
+
#
|
|
14
|
+
# Distinct from {Account}, which describes the *account* a number belongs to rather than
|
|
15
|
+
# how that number presents itself, and from {Whatsapp::Messages}, which is what actually
|
|
16
|
+
# reaches a user once the profile is set.
|
|
17
|
+
# See `docs/business_phone_number/profile.md`.
|
|
18
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
|
|
19
|
+
module Profile
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Registers a business phone number with Cloud API, consuming its two-step
|
|
6
|
+
# verification PIN. This is the prerequisite for every other endpoint in this
|
|
7
|
+
# gem — a number cannot send, receive, or be otherwise addressed until it is
|
|
8
|
+
# registered.
|
|
9
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration
|
|
10
|
+
class Register
|
|
11
|
+
include ActiveModel::Validations
|
|
12
|
+
extend ResponseHandling
|
|
13
|
+
extend Transport
|
|
14
|
+
|
|
15
|
+
module Defaults
|
|
16
|
+
MESSAGING_PRODUCT = "whatsapp"
|
|
17
|
+
EDGE = "register"
|
|
18
|
+
PIN_PATTERN = /\A\d{6}\z/
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The regions Meta supports for local storage, grouped as Meta's docs group
|
|
22
|
+
# them. 2-letter ISO 3166 country codes.
|
|
23
|
+
module DataLocalizationRegions
|
|
24
|
+
APAC = %w[AU ID IN JP SG KR].freeze
|
|
25
|
+
EUROPE = %w[DE CH GB].freeze
|
|
26
|
+
LATAM = %w[BR].freeze
|
|
27
|
+
MEA = %w[BH ZA AE].freeze
|
|
28
|
+
NORAM = %w[CA].freeze
|
|
29
|
+
|
|
30
|
+
ALL = [*APAC, *EUROPE, *LATAM, *MEA, *NORAM].freeze
|
|
31
|
+
|
|
32
|
+
# Normalizes caller input to a canonical uppercase region code.
|
|
33
|
+
# Unrecognized values are returned untouched so the inclusion validator can
|
|
34
|
+
# report them.
|
|
35
|
+
# @param value [String, Symbol, nil]
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
def self.normalize(value)
|
|
38
|
+
return if value.nil?
|
|
39
|
+
|
|
40
|
+
candidate = value.to_s.upcase
|
|
41
|
+
ALL.include?(candidate) ? candidate : value.to_s
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @!attribute [rw] pin
|
|
46
|
+
# @return [String] The 6-digit two-step verification PIN.
|
|
47
|
+
attr_accessor :pin
|
|
48
|
+
|
|
49
|
+
# @!attribute [rw] data_localization_region
|
|
50
|
+
# @return [String, nil] A 2-letter ISO 3166 country code enabling local storage.
|
|
51
|
+
attr_accessor :data_localization_region
|
|
52
|
+
|
|
53
|
+
validate :validate_pin
|
|
54
|
+
validates :data_localization_region, inclusion: { in: DataLocalizationRegions::ALL }, allow_nil: true
|
|
55
|
+
|
|
56
|
+
# @param pin [String, Integer] The 6-digit two-step verification PIN. If
|
|
57
|
+
# two-step verification is already enabled on the number, this must be the
|
|
58
|
+
# existing PIN.
|
|
59
|
+
# @param data_localization_region [String, Symbol, nil] One of
|
|
60
|
+
# {DataLocalizationRegions::ALL}, either casing.
|
|
61
|
+
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
62
|
+
def initialize(pin:, data_localization_region: nil)
|
|
63
|
+
@pin = pin
|
|
64
|
+
@data_localization_region = DataLocalizationRegions.normalize(data_localization_region)
|
|
65
|
+
|
|
66
|
+
validate!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Hash] The registration payload.
|
|
70
|
+
def serialize
|
|
71
|
+
{ messaging_product: Defaults::MESSAGING_PRODUCT, pin: pin.to_s, data_localization_region: }.compact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Redacts the pin so it never leaks into logs or console output.
|
|
75
|
+
# @return [String]
|
|
76
|
+
def inspect
|
|
77
|
+
"#<#{self.class.name} pin=[REDACTED] data_localization_region=#{data_localization_region.inspect}>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
# @param pin [String, Integer] See {#initialize}.
|
|
82
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
83
|
+
# @param data_localization_region [String, Symbol, nil] See {#initialize}.
|
|
84
|
+
# @return [Response]
|
|
85
|
+
# @raise [ActiveModel::ValidationError] if the pin or region is invalid.
|
|
86
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
87
|
+
def call(pin:, client: Client.new, data_localization_region: nil)
|
|
88
|
+
body = new(pin:, data_localization_region:).serialize
|
|
89
|
+
response = client.connection.post(edge_path(client, Defaults::EDGE), json: body)
|
|
90
|
+
|
|
91
|
+
Response.deserialize(
|
|
92
|
+
parse_json(handle_response!(response, error_class: Error, action: "register business phone number"))
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# @return [void]
|
|
100
|
+
def validate_pin
|
|
101
|
+
errors.add(:pin, "must be exactly 6 digits") unless pin.to_s.match?(Defaults::PIN_PATTERN)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Requests a verification code (by SMS or voice call) for a business phone number —
|
|
6
|
+
# the first step of the onboarding flow this module wraps end to end:
|
|
7
|
+
# `RequestCode` -> {VerifyCode} -> {Register}.
|
|
8
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/phone-number-verification-request-code-api
|
|
9
|
+
class RequestCode
|
|
10
|
+
include ActiveModel::Validations
|
|
11
|
+
extend ResponseHandling
|
|
12
|
+
extend Transport
|
|
13
|
+
|
|
14
|
+
module Defaults
|
|
15
|
+
EDGE = "request_code"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# How Meta delivers the verification code.
|
|
19
|
+
module CodeMethods
|
|
20
|
+
SMS = "SMS"
|
|
21
|
+
VOICE = "VOICE"
|
|
22
|
+
|
|
23
|
+
ALL = [SMS, VOICE].freeze
|
|
24
|
+
|
|
25
|
+
# Normalizes caller input to a canonical uppercase code method.
|
|
26
|
+
# Unrecognized values are returned as a string (preserving the original
|
|
27
|
+
# casing) so the inclusion validator can reject them.
|
|
28
|
+
# @param value [String, Symbol, nil]
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
def self.normalize(value)
|
|
31
|
+
return if value.nil?
|
|
32
|
+
|
|
33
|
+
candidate = value.to_s.upcase
|
|
34
|
+
ALL.include?(candidate) ? candidate : value.to_s
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @!attribute [rw] code_method
|
|
39
|
+
# @return [String] One of {CodeMethods::ALL}.
|
|
40
|
+
attr_accessor :code_method
|
|
41
|
+
|
|
42
|
+
# @!attribute [rw] language
|
|
43
|
+
# @return [String] The locale for the verification message (e.g. `"en_US"`).
|
|
44
|
+
attr_accessor :language
|
|
45
|
+
|
|
46
|
+
validates :code_method, presence: true, inclusion: { in: CodeMethods::ALL }
|
|
47
|
+
validates :language, presence: true
|
|
48
|
+
|
|
49
|
+
# @param code_method [String, Symbol] One of {CodeMethods::ALL}, either casing.
|
|
50
|
+
# @param language [String] The locale for the verification message. Meta
|
|
51
|
+
# publishes no enum for this field, so only presence is checked — unlike
|
|
52
|
+
# {Register}'s `pin`, which has a documented format.
|
|
53
|
+
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
54
|
+
def initialize(code_method:, language:)
|
|
55
|
+
@code_method = CodeMethods.normalize(code_method)
|
|
56
|
+
@language = language
|
|
57
|
+
|
|
58
|
+
validate!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Hash] The request-code payload. Both fields are documented required,
|
|
62
|
+
# so neither is ever compacted away.
|
|
63
|
+
def serialize
|
|
64
|
+
{ code_method:, language: }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
# @param code_method [String, Symbol] See {#initialize}.
|
|
69
|
+
# @param language [String] See {#initialize}.
|
|
70
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
71
|
+
# @return [Response]
|
|
72
|
+
# @raise [ActiveModel::ValidationError] if the code method or language is invalid.
|
|
73
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
74
|
+
def call(code_method:, language:, client: Client.new)
|
|
75
|
+
body = new(code_method:, language:).serialize
|
|
76
|
+
response = client.connection.post(edge_path(client, Defaults::EDGE), json: body)
|
|
77
|
+
|
|
78
|
+
Response.deserialize(
|
|
79
|
+
parse_json(handle_response!(response, error_class: Error, action: "request verification code"))
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# The response shared by every write action in this module: `{success}`, with an
|
|
6
|
+
# optional `id` that only `VerifyCode` ever populates. One class covers all of
|
|
7
|
+
# them — unlike {Whatsapp::SubscribedApp}'s `Response::*` namespace — since
|
|
8
|
+
# {Register}, {Deregister}, {RequestCode}, {VerifyCode}, {Account::Update}, and
|
|
9
|
+
# {Profile::Update} all return this same shape, `id` included or not.
|
|
10
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration
|
|
11
|
+
class Response
|
|
12
|
+
# @!attribute [rw] success
|
|
13
|
+
# @return [Boolean] Whether the action succeeded.
|
|
14
|
+
attr_accessor :success
|
|
15
|
+
|
|
16
|
+
# @!attribute [rw] id
|
|
17
|
+
# @return [String, nil] The phone number ID, only ever present on
|
|
18
|
+
# {VerifyCode}'s response.
|
|
19
|
+
attr_accessor :id
|
|
20
|
+
|
|
21
|
+
# @param success [Boolean]
|
|
22
|
+
# @param id [String, nil]
|
|
23
|
+
def initialize(success: false, id: nil)
|
|
24
|
+
@success = success
|
|
25
|
+
@id = id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# @param response [Hash, nil] The parsed response body.
|
|
30
|
+
# @return [Response]
|
|
31
|
+
def deserialize(response)
|
|
32
|
+
response ||= {}
|
|
33
|
+
|
|
34
|
+
new(success: response["success"] == true, id: response["id"])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# The one piece shared by {Register}, {Deregister}, {RequestCode}, {VerifyCode}, and
|
|
6
|
+
# {Profile}: all of them address a phone-number-scoped edge, differing only in edge name
|
|
7
|
+
# and body. Extended (not included) because {#edge_path} is used from class-level `.call`
|
|
8
|
+
# methods (as a private class method).
|
|
9
|
+
#
|
|
10
|
+
# The guard itself and its message live in {Whatsapp::PathBuilding}, shared with every
|
|
11
|
+
# other ID-scoped feature; what stays here is the ID this module addresses.
|
|
12
|
+
module Transport
|
|
13
|
+
include Whatsapp::PathBuilding
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# @param client [Whatsapp::Client]
|
|
18
|
+
# @param action [String] The edge to address ("register", "deregister",
|
|
19
|
+
# "request_code", "verify_code", or "whatsapp_business_profile").
|
|
20
|
+
# @return [String] The versioned path to the phone number's action edge.
|
|
21
|
+
# @raise [Error] if no phone number ID is configured.
|
|
22
|
+
def edge_path(client, action)
|
|
23
|
+
scoped_path(client, :phone_id, action, error_class: Error, purpose: "the #{action} edge")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Confirms the verification code sent by {RequestCode}, the second step of the
|
|
6
|
+
# onboarding flow this module wraps end to end: {RequestCode} -> `VerifyCode` ->
|
|
7
|
+
# {Register}.
|
|
8
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/verify-code-api
|
|
9
|
+
class VerifyCode
|
|
10
|
+
include ActiveModel::Validations
|
|
11
|
+
extend ResponseHandling
|
|
12
|
+
extend Transport
|
|
13
|
+
|
|
14
|
+
module Defaults
|
|
15
|
+
EDGE = "verify_code"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @!attribute [rw] code
|
|
19
|
+
# @return [String] The verification code received via SMS or voice call.
|
|
20
|
+
attr_accessor :code
|
|
21
|
+
|
|
22
|
+
validates :code, presence: true
|
|
23
|
+
|
|
24
|
+
# @param code [String, Integer] The verification code received via SMS or voice
|
|
25
|
+
# call. Meta documents this as a plain string with no format rule, unlike
|
|
26
|
+
# {Register}'s `pin`, so only presence is checked.
|
|
27
|
+
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
28
|
+
def initialize(code:)
|
|
29
|
+
@code = code
|
|
30
|
+
|
|
31
|
+
validate!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Hash] The verify-code payload.
|
|
35
|
+
def serialize
|
|
36
|
+
{ code: code.to_s }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Redacts the code so it never leaks into logs or console output, same as
|
|
40
|
+
# {Register#inspect} redacts the pin.
|
|
41
|
+
# @return [String]
|
|
42
|
+
def inspect
|
|
43
|
+
"#<#{self.class.name} code=[REDACTED]>"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# @param code [String, Integer] See {#initialize}.
|
|
48
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
49
|
+
# @return [Response]
|
|
50
|
+
# @raise [ActiveModel::ValidationError] if the code is blank.
|
|
51
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
52
|
+
def call(code:, client: Client.new)
|
|
53
|
+
body = new(code:).serialize
|
|
54
|
+
response = client.connection.post(edge_path(client, Defaults::EDGE), json: body)
|
|
55
|
+
|
|
56
|
+
Response.deserialize(
|
|
57
|
+
parse_json(handle_response!(response, error_class: Error, action: "verify code"))
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
# Onboards and deboards a business phone number with Cloud API: requesting and
|
|
5
|
+
# verifying a phone number's verification code, then registering or deregistering it.
|
|
6
|
+
#
|
|
7
|
+
# This is the switch that makes a phone number usable — or not — with Cloud API in
|
|
8
|
+
# the first place. It is a different concern from {Whatsapp::SubscribedApp}, which
|
|
9
|
+
# turns webhook delivery on and off for a whole WhatsApp Business Account: this
|
|
10
|
+
# module is per phone number and is a prerequisite for messaging itself, not just
|
|
11
|
+
# for notifications. The onboarding actions address `phone_id`, like
|
|
12
|
+
# {Whatsapp::Media} and {Whatsapp::Messages}.
|
|
13
|
+
#
|
|
14
|
+
# The full flow, in order: {RequestCode} -> {VerifyCode} -> {Register}, with
|
|
15
|
+
# {Deregister} as the reverse switch.
|
|
16
|
+
#
|
|
17
|
+
# {Profile} reads and updates the public card a WhatsApp user sees before they reply.
|
|
18
|
+
# It addresses `phone_id` like the onboarding actions above, so it shares their transport.
|
|
19
|
+
#
|
|
20
|
+
# {Account} is the one exception to the `phone_id` rule: it reads and updates the
|
|
21
|
+
# WhatsApp Business Account a number belongs to, so it addresses `waba_id` and
|
|
22
|
+
# carries its own transport.
|
|
23
|
+
# See `lib/ruby/whatsapp/business_phone_number/CLAUDE.md`,
|
|
24
|
+
# `docs/business_phone_number/README.md`, and `docs/business_phone_number/profile.md`.
|
|
25
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration
|
|
26
|
+
module BusinessPhoneNumber
|
|
27
|
+
class Error < Whatsapp::Error; end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -25,6 +25,7 @@ module Whatsapp
|
|
|
25
25
|
# templates.find(template_id: created.id).status # => "PENDING"
|
|
26
26
|
# Source: https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/message_templates/
|
|
27
27
|
class MessageTemplates
|
|
28
|
+
include PathBuilding
|
|
28
29
|
include ResponseHandling
|
|
29
30
|
|
|
30
31
|
class TemplateError < Whatsapp::Error; end
|
|
@@ -204,12 +205,7 @@ module Whatsapp
|
|
|
204
205
|
# @return [String] The versioned path for a WABA-scoped edge.
|
|
205
206
|
# @raise [TemplateError] if no WABA ID is configured.
|
|
206
207
|
def edge_path(edge)
|
|
207
|
-
|
|
208
|
-
raise TemplateError,
|
|
209
|
-
"waba_id is required for template management; set it via Whatsapp.configure or Client.new(waba_id:)"
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
client.path_for(client.waba_id, edge)
|
|
208
|
+
scoped_path(client, :waba_id, edge, error_class: TemplateError, purpose: "template management")
|
|
213
209
|
end
|
|
214
210
|
|
|
215
211
|
# @return [Hash] Only the editable fields that were supplied.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
# The ID guard and path build shared by every WABA- or phone-number-scoped feature.
|
|
5
|
+
#
|
|
6
|
+
# Each feature keeps its own transport with a named method — {Whatsapp::SubscribedApp::Transport}'s
|
|
7
|
+
# `edge_path`, {Whatsapp::BusinessPhoneNumber::Account::Transport}'s `node_path` — since those
|
|
8
|
+
# names are what keep an edge and a node from being confused at a call site. This holds only
|
|
9
|
+
# the part that was identical across all of them: refusing to address an unconfigured ID, and
|
|
10
|
+
# phrasing that refusal the same way every time.
|
|
11
|
+
module PathBuilding
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# @param client [Whatsapp::Client]
|
|
15
|
+
# @param id_name [Symbol] The client attribute to address (`:waba_id` or `:phone_id`).
|
|
16
|
+
# @param segments [Array<String>] Edge segments after the ID; pass none to address the node itself.
|
|
17
|
+
# @param error_class [Class] The feature's own error class, never {Whatsapp::RequestError}.
|
|
18
|
+
# @param purpose [String] What the caller was reaching for, e.g. `"subscribed apps"`.
|
|
19
|
+
# @return [String] The versioned path.
|
|
20
|
+
# @raise [Whatsapp::Error] the given error_class, if the ID is not configured.
|
|
21
|
+
def scoped_path(client, id_name, *segments, error_class:, purpose:)
|
|
22
|
+
id = client.public_send(id_name)
|
|
23
|
+
|
|
24
|
+
if id.nil? || id.to_s.empty?
|
|
25
|
+
raise error_class,
|
|
26
|
+
"#{id_name} is required for #{purpose}; " \
|
|
27
|
+
"set it via Whatsapp.configure or Client.new(#{id_name}:)"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
client.path_for(id, *segments)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module SubscribedApp
|
|
5
|
+
# Lists the apps currently subscribed to a WABA's webhook notifications.
|
|
6
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/subscribed-apps-api
|
|
7
|
+
class List
|
|
8
|
+
extend ResponseHandling
|
|
9
|
+
extend Transport
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
13
|
+
# @param fields [Array<String>, String, nil] Restrict each app to these fields
|
|
14
|
+
# (`id`, `name`, `link`).
|
|
15
|
+
# @return [Response::Collection]
|
|
16
|
+
# @raise [Error] if no WABA ID is configured, or the request fails.
|
|
17
|
+
def call(client: Client.new, fields: nil)
|
|
18
|
+
params = fields ? { fields: Array(fields).join(",") } : {}
|
|
19
|
+
response = client.connection.get(edge_path(client), params:)
|
|
20
|
+
|
|
21
|
+
Response::Collection.deserialize(
|
|
22
|
+
parse_json(handle_response!(response, error_class: Error, action: "list subscribed apps"))
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|