ruby-whatsapp 0.4.0 → 0.6.1
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 +152 -0
- data/README.md +322 -617
- data/docs/README.md +76 -0
- data/docs/business_phone_number/README.md +243 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +177 -0
- data/docs/errors.md +219 -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 +215 -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 +212 -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 +342 -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 +193 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/schedule/README.md +183 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +217 -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/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +107 -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 +520 -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 +91 -0
- data/docs/webhooks/smb_message_echoes.md +91 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +87 -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/contact_book/delete.rb +87 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -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/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
- data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
- data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
- data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
- data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
- data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
- data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
- data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/schedule/create.rb +101 -0
- data/lib/ruby/whatsapp/schedule/list.rb +50 -0
- data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
- data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
- data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
- data/lib/ruby/whatsapp/schedule/response.rb +18 -0
- data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
- data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
- data/lib/ruby/whatsapp/schedule.rb +15 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
- data/lib/ruby/whatsapp/webhook/change.rb +3 -1
- data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
- data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
- data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
- data/lib/ruby/whatsapp/webhook/history.rb +73 -34
- data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
- data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
- data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
- data/lib/ruby/whatsapp/webhook/message.rb +3 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
- data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
- data/lib/ruby/whatsapp/webhook/status.rb +23 -2
- data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
- data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
- data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
- data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
- metadata +100 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Account
|
|
6
|
+
# The WhatsApp Business Account node returned by {Get}.
|
|
7
|
+
#
|
|
8
|
+
# Only `id` and `name` are documented required; everything else arrives just when
|
|
9
|
+
# asked for via `fields`, so every attribute tolerates being absent. Status values
|
|
10
|
+
# are exposed as raw strings and are never validated on the way in — this is
|
|
11
|
+
# read-side data, and a value Meta adds later must still round-trip. The frozen
|
|
12
|
+
# constant modules below are for comparison, not enforcement, mirroring how
|
|
13
|
+
# {Whatsapp::MessageTemplates::Response::Node} keeps Meta's echoed payloads raw.
|
|
14
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
|
|
15
|
+
class Details
|
|
16
|
+
# Meta's review status for the account.
|
|
17
|
+
module ReviewStatuses
|
|
18
|
+
APPROVED = "APPROVED"
|
|
19
|
+
DEFERRED = "DEFERRED"
|
|
20
|
+
PENDING = "PENDING"
|
|
21
|
+
REJECTED = "REJECTED"
|
|
22
|
+
|
|
23
|
+
ALL = [APPROVED, DEFERRED, PENDING, REJECTED].freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Where the account sits in Meta's business verification process.
|
|
27
|
+
module VerificationStatuses
|
|
28
|
+
EXPIRED = "EXPIRED"
|
|
29
|
+
FAILED = "FAILED"
|
|
30
|
+
INELIGIBLE = "INELIGIBLE"
|
|
31
|
+
NOT_VERIFIED = "NOT_VERIFIED"
|
|
32
|
+
PENDING = "PENDING"
|
|
33
|
+
PENDING_NEED_MORE_INFO = "PENDING_NEED_MORE_INFO"
|
|
34
|
+
PENDING_SUBMISSION = "PENDING_SUBMISSION"
|
|
35
|
+
REJECTED = "REJECTED"
|
|
36
|
+
REVOKED = "REVOKED"
|
|
37
|
+
VERIFIED = "VERIFIED"
|
|
38
|
+
|
|
39
|
+
ALL = [EXPIRED, FAILED, INELIGIBLE, NOT_VERIFIED, PENDING, PENDING_NEED_MORE_INFO,
|
|
40
|
+
PENDING_SUBMISSION, REJECTED, REVOKED, VERIFIED,].freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Who owns the account: your own business, a client's, or a partner acting for one.
|
|
44
|
+
module OwnershipTypes
|
|
45
|
+
CLIENT_OWNED = "CLIENT_OWNED"
|
|
46
|
+
ON_BEHALF_OF = "ON_BEHALF_OF"
|
|
47
|
+
SELF = "SELF"
|
|
48
|
+
|
|
49
|
+
ALL = [CLIENT_OWNED, ON_BEHALF_OF, SELF].freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @!attribute [rw] id
|
|
53
|
+
# @return [String, nil] The account's unique identifier.
|
|
54
|
+
attr_accessor :id
|
|
55
|
+
|
|
56
|
+
# @!attribute [rw] name
|
|
57
|
+
# @return [String, nil] The account's human-readable name.
|
|
58
|
+
attr_accessor :name
|
|
59
|
+
|
|
60
|
+
# @!attribute [rw] timezone_id
|
|
61
|
+
# @return [String, nil] The account's timezone identifier.
|
|
62
|
+
attr_accessor :timezone_id
|
|
63
|
+
|
|
64
|
+
# @!attribute [rw] message_template_namespace
|
|
65
|
+
# @return [String, nil] The namespace the account's templates live in.
|
|
66
|
+
attr_accessor :message_template_namespace
|
|
67
|
+
|
|
68
|
+
# @!attribute [rw] account_review_status
|
|
69
|
+
# @return [String, nil] One of {ReviewStatuses::ALL}.
|
|
70
|
+
attr_accessor :account_review_status
|
|
71
|
+
|
|
72
|
+
# @!attribute [rw] business_verification_status
|
|
73
|
+
# @return [String, nil] One of {VerificationStatuses::ALL}.
|
|
74
|
+
attr_accessor :business_verification_status
|
|
75
|
+
|
|
76
|
+
# @!attribute [rw] country
|
|
77
|
+
# @return [String, nil] The account's country code.
|
|
78
|
+
attr_accessor :country
|
|
79
|
+
|
|
80
|
+
# @!attribute [rw] ownership_type
|
|
81
|
+
# @return [String, nil] One of {OwnershipTypes::ALL}.
|
|
82
|
+
attr_accessor :ownership_type
|
|
83
|
+
|
|
84
|
+
# @!attribute [rw] primary_business_location
|
|
85
|
+
# @return [String, nil] The account's primary business location.
|
|
86
|
+
attr_accessor :primary_business_location
|
|
87
|
+
|
|
88
|
+
# @param id [String, nil]
|
|
89
|
+
# @param name [String, nil]
|
|
90
|
+
# @param timezone_id [String, nil]
|
|
91
|
+
# @param message_template_namespace [String, nil]
|
|
92
|
+
# @param account_review_status [String, nil]
|
|
93
|
+
# @param business_verification_status [String, nil]
|
|
94
|
+
# @param country [String, nil]
|
|
95
|
+
# @param ownership_type [String, nil]
|
|
96
|
+
# @param primary_business_location [String, nil]
|
|
97
|
+
def initialize(id: nil, name: nil, timezone_id: nil, message_template_namespace: nil,
|
|
98
|
+
account_review_status: nil, business_verification_status: nil, country: nil,
|
|
99
|
+
ownership_type: nil, primary_business_location: nil)
|
|
100
|
+
@id = id
|
|
101
|
+
@name = name
|
|
102
|
+
@timezone_id = timezone_id
|
|
103
|
+
@message_template_namespace = message_template_namespace
|
|
104
|
+
@account_review_status = account_review_status
|
|
105
|
+
@business_verification_status = business_verification_status
|
|
106
|
+
@country = country
|
|
107
|
+
@ownership_type = ownership_type
|
|
108
|
+
@primary_business_location = primary_business_location
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class << self
|
|
112
|
+
# @param response [Hash, nil] The parsed response body.
|
|
113
|
+
# @return [Details]
|
|
114
|
+
def deserialize(response)
|
|
115
|
+
response ||= {}
|
|
116
|
+
|
|
117
|
+
new(
|
|
118
|
+
id: response["id"],
|
|
119
|
+
name: response["name"],
|
|
120
|
+
timezone_id: response["timezone_id"],
|
|
121
|
+
message_template_namespace: response["message_template_namespace"],
|
|
122
|
+
account_review_status: response["account_review_status"],
|
|
123
|
+
business_verification_status: response["business_verification_status"],
|
|
124
|
+
country: response["country"],
|
|
125
|
+
ownership_type: response["ownership_type"],
|
|
126
|
+
primary_business_location: response["primary_business_location"]
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [Boolean] Whether Meta has approved the account.
|
|
132
|
+
def approved?
|
|
133
|
+
account_review_status == ReviewStatuses::APPROVED
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Boolean] Whether the business behind the account is verified.
|
|
137
|
+
def verified?
|
|
138
|
+
business_verification_status == VerificationStatuses::VERIFIED
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Boolean] Whether the account belongs to your own business, rather
|
|
142
|
+
# than to a client or a partner acting on one's behalf.
|
|
143
|
+
def self_owned?
|
|
144
|
+
ownership_type == OwnershipTypes::SELF
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Account
|
|
6
|
+
# Reads a WhatsApp Business Account's details: its configuration, review and
|
|
7
|
+
# verification status, and ownership.
|
|
8
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
|
|
9
|
+
class Get
|
|
10
|
+
extend ResponseHandling
|
|
11
|
+
extend Transport
|
|
12
|
+
|
|
13
|
+
# The fields Meta documents for this node. Offered for convenience, not
|
|
14
|
+
# enforcement — {.call} deliberately does not validate `fields` against this
|
|
15
|
+
# list (nor does {Whatsapp::SubscribedApp::List}), so a field Meta adds later
|
|
16
|
+
# works without a gem release.
|
|
17
|
+
module Fields
|
|
18
|
+
ID = "id"
|
|
19
|
+
NAME = "name"
|
|
20
|
+
TIMEZONE_ID = "timezone_id"
|
|
21
|
+
MESSAGE_TEMPLATE_NAMESPACE = "message_template_namespace"
|
|
22
|
+
ACCOUNT_REVIEW_STATUS = "account_review_status"
|
|
23
|
+
BUSINESS_VERIFICATION_STATUS = "business_verification_status"
|
|
24
|
+
COUNTRY = "country"
|
|
25
|
+
OWNERSHIP_TYPE = "ownership_type"
|
|
26
|
+
PRIMARY_BUSINESS_LOCATION = "primary_business_location"
|
|
27
|
+
|
|
28
|
+
ALL = [ID, NAME, TIMEZONE_ID, MESSAGE_TEMPLATE_NAMESPACE, ACCOUNT_REVIEW_STATUS,
|
|
29
|
+
BUSINESS_VERIFICATION_STATUS, COUNTRY, OWNERSHIP_TYPE, PRIMARY_BUSINESS_LOCATION,].freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# @example Read everything Meta publishes for the account
|
|
34
|
+
# Get.call(fields: Get::Fields::ALL)
|
|
35
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
36
|
+
# @param fields [Array<String>, String, nil] Restrict the response to these
|
|
37
|
+
# fields; see {Fields::ALL}. Omitted entirely when nil, in which case Meta
|
|
38
|
+
# returns only its own defaults (`id` and `name`).
|
|
39
|
+
# @return [Details]
|
|
40
|
+
# @raise [Error] if no WABA ID is configured, or the request fails.
|
|
41
|
+
def call(client: Client.new, fields: nil)
|
|
42
|
+
params = fields ? { fields: Array(fields).join(",") } : {}
|
|
43
|
+
response = client.connection.get(node_path(client), params:)
|
|
44
|
+
|
|
45
|
+
Details.deserialize(
|
|
46
|
+
parse_json(handle_response!(response, error_class: Error, action: "get business account details"))
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Account
|
|
6
|
+
# The one piece shared by {Get} and {Update}: both address the WABA node itself,
|
|
7
|
+
# differing only in HTTP verb and body. Extended (not included) since those
|
|
8
|
+
# classes are class-method-only.
|
|
9
|
+
#
|
|
10
|
+
# Deliberately separate from {BusinessPhoneNumber::Transport}, which this shadows
|
|
11
|
+
# by lexical scope inside {Account}. That one guards `phone_id` and always appends
|
|
12
|
+
# an edge segment; this addresses `waba_id` and no edge at all. Both delegate the
|
|
13
|
+
# guard and its wording to {Whatsapp::PathBuilding}, so the method names are now the
|
|
14
|
+
# whole of the difference — `node_path` here, `edge_path` there, so the two can never
|
|
15
|
+
# be confused at a call site.
|
|
16
|
+
module Transport
|
|
17
|
+
include Whatsapp::PathBuilding
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# @param client [Whatsapp::Client]
|
|
22
|
+
# @return [String] The versioned path to the WABA node, with no edge segment.
|
|
23
|
+
# @raise [Error] if no WABA ID is configured.
|
|
24
|
+
def node_path(client)
|
|
25
|
+
scoped_path(client, :waba_id, error_class: Error, purpose: "business account details")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Account
|
|
6
|
+
# Updates a WhatsApp Business Account's name or timezone.
|
|
7
|
+
#
|
|
8
|
+
# A validated instance rather than a bare class method (unlike {Get}, which has
|
|
9
|
+
# nothing to check): Meta documents `name` as a non-empty string, and since both
|
|
10
|
+
# fields are optional an all-nil call would spend a request to change nothing.
|
|
11
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
|
|
12
|
+
class Update
|
|
13
|
+
include ActiveModel::Validations
|
|
14
|
+
extend ResponseHandling
|
|
15
|
+
extend Transport
|
|
16
|
+
|
|
17
|
+
# @!attribute [rw] name
|
|
18
|
+
# @return [String, nil] The account's new name.
|
|
19
|
+
attr_accessor :name
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] timezone_id
|
|
22
|
+
# @return [String, nil] The account's new timezone identifier.
|
|
23
|
+
attr_accessor :timezone_id
|
|
24
|
+
|
|
25
|
+
validate :validate_any_attribute_present
|
|
26
|
+
validates :name, presence: true, allow_nil: true
|
|
27
|
+
validates :timezone_id, presence: true, allow_nil: true
|
|
28
|
+
|
|
29
|
+
# @param name [String, nil] The account's new name. Meta rejects an empty
|
|
30
|
+
# string, so a blank value fails here rather than at the API.
|
|
31
|
+
# @param timezone_id [String, nil] The account's new timezone identifier. Meta
|
|
32
|
+
# publishes no enum for this field, so only presence is checked.
|
|
33
|
+
# @raise [ActiveModel::ValidationError] if both are nil, or either is blank.
|
|
34
|
+
def initialize(name: nil, timezone_id: nil)
|
|
35
|
+
@name = name
|
|
36
|
+
@timezone_id = timezone_id
|
|
37
|
+
|
|
38
|
+
validate!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Hash] The update payload. Both fields are optional, so an omitted
|
|
42
|
+
# one is compacted away rather than sent as null — which Meta would read as
|
|
43
|
+
# an instruction to blank it.
|
|
44
|
+
def serialize
|
|
45
|
+
{ name:, timezone_id: }.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
50
|
+
# @param name [String, nil] See {#initialize}.
|
|
51
|
+
# @param timezone_id [String, nil] See {#initialize}.
|
|
52
|
+
# @return [BusinessPhoneNumber::Response]
|
|
53
|
+
# @raise [ActiveModel::ValidationError] if no attribute is given, or one is blank.
|
|
54
|
+
# @raise [Error] if no WABA ID is configured, or the request fails.
|
|
55
|
+
def call(client: Client.new, name: nil, timezone_id: nil)
|
|
56
|
+
body = new(name:, timezone_id:).serialize
|
|
57
|
+
response = client.connection.post(node_path(client), json: body)
|
|
58
|
+
|
|
59
|
+
BusinessPhoneNumber::Response.deserialize(
|
|
60
|
+
parse_json(handle_response!(response, error_class: Error, action: "update business account"))
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @return [void]
|
|
68
|
+
def validate_any_attribute_present
|
|
69
|
+
return unless name.nil? && timezone_id.nil?
|
|
70
|
+
|
|
71
|
+
errors.add(:base, "at least one of name or timezone_id must be provided")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Reads and updates the WhatsApp Business Account node itself — the account a
|
|
6
|
+
# business phone number belongs to, as opposed to the number's own onboarding
|
|
7
|
+
# state that the rest of this module drives.
|
|
8
|
+
#
|
|
9
|
+
# This is the one part of {Whatsapp::BusinessPhoneNumber} that addresses `waba_id`
|
|
10
|
+
# rather than `phone_id`, so it carries its own {Transport} and cannot share
|
|
11
|
+
# {BusinessPhoneNumber::Transport}. It is also the only endpoint pair here that
|
|
12
|
+
# touches the account rather than the number: {Get} reads review, verification, and
|
|
13
|
+
# ownership state; {Update} renames the account or moves its timezone.
|
|
14
|
+
#
|
|
15
|
+
# Distinct from {Whatsapp::SubscribedApp}, which addresses the same `waba_id` but
|
|
16
|
+
# only toggles webhook delivery, and from {Whatsapp::MessageTemplates}, which
|
|
17
|
+
# manages the templates hanging off that account.
|
|
18
|
+
# See `docs/business_phone_number/account.md`.
|
|
19
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
|
|
20
|
+
module Account
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module ContactBook
|
|
6
|
+
# Deletes one user's entry from your contact book.
|
|
7
|
+
#
|
|
8
|
+
# A validated instance rather than a bare class method: Meta documents a rule this
|
|
9
|
+
# can check — parent BSUIDs are not accepted here — and the edge is rate limited, so
|
|
10
|
+
# a request that was never going to be accepted should not be spent.
|
|
11
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
12
|
+
class Delete
|
|
13
|
+
include ActiveModel::Validations
|
|
14
|
+
extend ResponseHandling
|
|
15
|
+
extend Transport
|
|
16
|
+
|
|
17
|
+
module Defaults
|
|
18
|
+
EDGE = "contact_book"
|
|
19
|
+
MESSAGING_PRODUCT = "whatsapp"
|
|
20
|
+
# A parent BSUID carries an extra segment (`US.ENT.118...`) where a standard one
|
|
21
|
+
# has just the country code and identifier (`US.134...`), so counting separators
|
|
22
|
+
# distinguishes them without asserting a country-code grammar Meta has not
|
|
23
|
+
# published. See {#validate_standard_bsuid}.
|
|
24
|
+
PARENT_SEPARATOR_COUNT = 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @!attribute [rw] bsuid
|
|
28
|
+
# @return [String] The business-scoped user ID whose entry to remove.
|
|
29
|
+
attr_accessor :bsuid
|
|
30
|
+
|
|
31
|
+
validate :validate_standard_bsuid
|
|
32
|
+
validates :bsuid, presence: true
|
|
33
|
+
|
|
34
|
+
# @param bsuid [String] The user's standard BSUID, e.g.
|
|
35
|
+
# `"US.13491208655302741918"`. Must belong to the same business portfolio as the
|
|
36
|
+
# phone number — Meta checks that server-side, since it is account state this gem
|
|
37
|
+
# cannot see. Parent BSUIDs are not supported and are rejected locally.
|
|
38
|
+
# @raise [ActiveModel::ValidationError] if the BSUID is missing or looks like a parent.
|
|
39
|
+
def initialize(bsuid:)
|
|
40
|
+
@bsuid = bsuid
|
|
41
|
+
|
|
42
|
+
validate!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Hash] The query parameters. Sent as a query string rather than a body
|
|
46
|
+
# because the edge is a `DELETE` — the one action in this directory whose
|
|
47
|
+
# `#serialize` is not a JSON payload. `messaging_product` is documented required,
|
|
48
|
+
# so it is always sent.
|
|
49
|
+
def serialize
|
|
50
|
+
{ messaging_product: Defaults::MESSAGING_PRODUCT, bsuid: }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# @example Forget a user who asked to be forgotten
|
|
55
|
+
# ContactBook::Delete.call(bsuid: "US.13491208655302741918")
|
|
56
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
57
|
+
# @param bsuid [String] See {#initialize}.
|
|
58
|
+
# @return [Deletion] Whose `deleted` is `false` if no entry existed.
|
|
59
|
+
# @raise [ActiveModel::ValidationError] if the BSUID is missing or looks like a parent.
|
|
60
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
61
|
+
def call(bsuid:, client: Client.new)
|
|
62
|
+
params = new(bsuid:).serialize
|
|
63
|
+
response = client.connection.delete(edge_path(client, Defaults::EDGE), params:)
|
|
64
|
+
|
|
65
|
+
Deletion.deserialize(
|
|
66
|
+
parse_json(handle_response!(response, error_class: Error, action: "delete contact book entry"))
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Meta states plainly that parent BSUIDs are rejected here, and this release hands
|
|
74
|
+
# callers `user_id` and `parent_user_id` side by side on half a dozen webhook
|
|
75
|
+
# classes, so mixing them up is the predictable mistake. Detected by segment count
|
|
76
|
+
# rather than by matching `ENT`, which would bet that `ENT` is the only marker Meta
|
|
77
|
+
# will ever insert — an inference either way, so the weaker one wins.
|
|
78
|
+
# @return [void]
|
|
79
|
+
def validate_standard_bsuid
|
|
80
|
+
return if bsuid.nil? || bsuid.to_s.count(".") <= Defaults::PARENT_SEPARATOR_COUNT
|
|
81
|
+
|
|
82
|
+
errors.add(:bsuid, "looks like a parent BSUID; the contact book accepts standard BSUIDs only")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module ContactBook
|
|
6
|
+
# The result of a {Delete}: whether the request worked, and whether it found anything.
|
|
7
|
+
#
|
|
8
|
+
# Its own class rather than {BusinessPhoneNumber::Response}, which carries only
|
|
9
|
+
# `success` (plus the `id` {VerifyCode} sets). `deleted` is a second, independent
|
|
10
|
+
# boolean that changes how `success` reads — `success: true, deleted: false` means
|
|
11
|
+
# the request was fine and there was simply no entry — and collapsing the two would
|
|
12
|
+
# lose that. Adding it to the class every onboarding action returns would also put a
|
|
13
|
+
# field on five actions that can never set it.
|
|
14
|
+
class Deletion
|
|
15
|
+
# @!attribute [rw] messaging_product
|
|
16
|
+
# @return [String, nil] Always `"whatsapp"`.
|
|
17
|
+
attr_accessor :messaging_product
|
|
18
|
+
|
|
19
|
+
# @!attribute [rw] success
|
|
20
|
+
# @return [Boolean] Whether the request was processed.
|
|
21
|
+
attr_accessor :success
|
|
22
|
+
|
|
23
|
+
# @!attribute [rw] deleted
|
|
24
|
+
# @return [Boolean] Whether an entry existed and was removed. `false` alongside a
|
|
25
|
+
# `true` {#success} means there was nothing stored for that BSUID — not a failure.
|
|
26
|
+
attr_accessor :deleted
|
|
27
|
+
|
|
28
|
+
def initialize(messaging_product: nil, success: false, deleted: false)
|
|
29
|
+
@messaging_product = messaging_product
|
|
30
|
+
@success = success
|
|
31
|
+
@deleted = deleted
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# @param response [Hash, nil] The parsed response body.
|
|
36
|
+
# @return [Deletion]
|
|
37
|
+
def deserialize(response)
|
|
38
|
+
response ||= {}
|
|
39
|
+
|
|
40
|
+
new(
|
|
41
|
+
messaging_product: response["messaging_product"],
|
|
42
|
+
success: response["success"] == true,
|
|
43
|
+
deleted: response["deleted"] == true
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
# Removes a stored user contact from your **contact book** — the store Meta keeps of
|
|
6
|
+
# the phone numbers and BSUIDs you have interacted with, and uses to keep putting a
|
|
7
|
+
# user's phone number in your webhooks after they adopt a username.
|
|
8
|
+
#
|
|
9
|
+
# Addressed as a `phone_id` edge (`DELETE /{phone_id}/contact_book`), so it reuses
|
|
10
|
+
# {BusinessPhoneNumber::Transport} unchanged, like {Profile} and {Username}. It sits
|
|
11
|
+
# here rather than in a top-level module for two reasons: this directory is the
|
|
12
|
+
# administration of one phone number, as opposed to {Whatsapp::Media} and
|
|
13
|
+
# {Whatsapp::Messages}, which are the traffic itself; and a top-level module would
|
|
14
|
+
# need its own error class for a single endpoint, where living here means one
|
|
15
|
+
# `rescue BusinessPhoneNumber::Error` still covers the whole directory.
|
|
16
|
+
#
|
|
17
|
+
# Note the asymmetry between how it is addressed and what it affects: the edge is on
|
|
18
|
+
# one phone number, but the contact book is scoped to the whole **business portfolio**,
|
|
19
|
+
# so a deletion removes that user's phone number from webhooks for *every* number in
|
|
20
|
+
# the portfolio. See `docs/business_phone_number/contact_book.md`.
|
|
21
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
22
|
+
module ContactBook
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Profile
|
|
6
|
+
# The business profile returned by {Get}.
|
|
7
|
+
#
|
|
8
|
+
# Every field arrives only when asked for via `fields`, so every attribute tolerates
|
|
9
|
+
# being absent. Nothing is validated or normalized on the way in — this is read-side
|
|
10
|
+
# data, and a `vertical` Meta adds later must still round-trip untouched. Compare
|
|
11
|
+
# against {Verticals::ALL} rather than expecting this class to have rejected an unknown
|
|
12
|
+
# value, the same reasoning as {Account::Details}' statuses and
|
|
13
|
+
# {Whatsapp::MessageTemplates::Response::Node}'s raw components.
|
|
14
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
|
|
15
|
+
class Details
|
|
16
|
+
# @!attribute [rw] messaging_product
|
|
17
|
+
# @return [String, nil] The messaging service, always `"whatsapp"` in practice.
|
|
18
|
+
attr_accessor :messaging_product
|
|
19
|
+
|
|
20
|
+
# @!attribute [rw] about
|
|
21
|
+
# @return [String, nil] The text shown in the profile's About section.
|
|
22
|
+
attr_accessor :about
|
|
23
|
+
|
|
24
|
+
# @!attribute [rw] address
|
|
25
|
+
# @return [String, nil] The business's physical address.
|
|
26
|
+
attr_accessor :address
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] description
|
|
29
|
+
# @return [String, nil] The business description.
|
|
30
|
+
attr_accessor :description
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] email
|
|
33
|
+
# @return [String, nil] The business's contact email address.
|
|
34
|
+
attr_accessor :email
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] profile_picture_url
|
|
37
|
+
# @return [String, nil] The URL of the profile picture.
|
|
38
|
+
attr_accessor :profile_picture_url
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] websites
|
|
41
|
+
# @return [Array<String>, nil] The business's website URLs.
|
|
42
|
+
attr_accessor :websites
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] vertical
|
|
45
|
+
# @return [String, nil] One of {Verticals::ALL}, as sent by Meta.
|
|
46
|
+
attr_accessor :vertical
|
|
47
|
+
|
|
48
|
+
# @param messaging_product [String, nil]
|
|
49
|
+
# @param about [String, nil]
|
|
50
|
+
# @param address [String, nil]
|
|
51
|
+
# @param description [String, nil]
|
|
52
|
+
# @param email [String, nil]
|
|
53
|
+
# @param profile_picture_url [String, nil]
|
|
54
|
+
# @param websites [Array<String>, nil]
|
|
55
|
+
# @param vertical [String, nil]
|
|
56
|
+
def initialize(messaging_product: nil, about: nil, address: nil, description: nil,
|
|
57
|
+
email: nil, profile_picture_url: nil, websites: nil, vertical: nil)
|
|
58
|
+
@messaging_product = messaging_product
|
|
59
|
+
@about = about
|
|
60
|
+
@address = address
|
|
61
|
+
@description = description
|
|
62
|
+
@email = email
|
|
63
|
+
@profile_picture_url = profile_picture_url
|
|
64
|
+
@websites = websites
|
|
65
|
+
@vertical = vertical
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
# @param response [Hash, nil] The parsed response body, envelope included.
|
|
70
|
+
# @return [Details]
|
|
71
|
+
def deserialize(response)
|
|
72
|
+
profile = unwrap(response)
|
|
73
|
+
|
|
74
|
+
new(
|
|
75
|
+
messaging_product: profile["messaging_product"],
|
|
76
|
+
about: profile["about"],
|
|
77
|
+
address: profile["address"],
|
|
78
|
+
description: profile["description"],
|
|
79
|
+
email: profile["email"],
|
|
80
|
+
profile_picture_url: profile["profile_picture_url"],
|
|
81
|
+
websites: profile["websites"],
|
|
82
|
+
vertical: profile["vertical"]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Digs the profile out of Meta's `data` envelope.
|
|
89
|
+
#
|
|
90
|
+
# The reference documents the entry as wrapping the profile in `business_profile`,
|
|
91
|
+
# while live responses are observed to carry the fields directly; both are read
|
|
92
|
+
# rather than betting on one. Only the first entry matters — a phone number has
|
|
93
|
+
# exactly one profile.
|
|
94
|
+
# @param response [Hash, nil]
|
|
95
|
+
# @return [Hash]
|
|
96
|
+
def unwrap(response)
|
|
97
|
+
entry = Array((response || {})["data"]).first || {}
|
|
98
|
+
|
|
99
|
+
entry["business_profile"] || entry
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module BusinessPhoneNumber
|
|
5
|
+
module Profile
|
|
6
|
+
# Reads a business profile: its about text, description, contact details, websites,
|
|
7
|
+
# industry vertical, and profile picture.
|
|
8
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
|
|
9
|
+
class Get
|
|
10
|
+
extend ResponseHandling
|
|
11
|
+
extend Transport
|
|
12
|
+
|
|
13
|
+
module Defaults
|
|
14
|
+
EDGE = "whatsapp_business_profile"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The fields Meta documents for this edge. Offered for convenience, not
|
|
18
|
+
# enforcement — {.call} deliberately does not validate `fields` against this list
|
|
19
|
+
# (nor do {Account::Get} or {Whatsapp::SubscribedApp::List}), so a field Meta adds
|
|
20
|
+
# later works without a gem release.
|
|
21
|
+
module Fields
|
|
22
|
+
MESSAGING_PRODUCT = "messaging_product"
|
|
23
|
+
ABOUT = "about"
|
|
24
|
+
ADDRESS = "address"
|
|
25
|
+
DESCRIPTION = "description"
|
|
26
|
+
EMAIL = "email"
|
|
27
|
+
PROFILE_PICTURE_URL = "profile_picture_url"
|
|
28
|
+
WEBSITES = "websites"
|
|
29
|
+
VERTICAL = "vertical"
|
|
30
|
+
|
|
31
|
+
ALL = [MESSAGING_PRODUCT, ABOUT, ADDRESS, DESCRIPTION, EMAIL, PROFILE_PICTURE_URL,
|
|
32
|
+
WEBSITES, VERTICAL,].freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# @example Read everything Meta publishes for the profile
|
|
37
|
+
# Get.call(fields: Get::Fields::ALL)
|
|
38
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
39
|
+
# @param fields [Array<String>, String, nil] Restrict the response to these
|
|
40
|
+
# fields; see {Fields::ALL}. Omitted entirely when nil, in which case Meta
|
|
41
|
+
# returns only its own defaults.
|
|
42
|
+
# @return [Details]
|
|
43
|
+
# @raise [Error] if no phone number ID is configured, or the request fails.
|
|
44
|
+
def call(client: Client.new, fields: nil)
|
|
45
|
+
params = fields ? { fields: Array(fields).join(",") } : {}
|
|
46
|
+
response = client.connection.get(edge_path(client, Defaults::EDGE), params:)
|
|
47
|
+
|
|
48
|
+
Details.deserialize(
|
|
49
|
+
parse_json(handle_response!(response, error_class: Error, action: "get business profile details"))
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|