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,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
# Lists a WABA's campaign schedules, one page at a time.
|
|
6
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
7
|
+
class List
|
|
8
|
+
extend ResponseHandling
|
|
9
|
+
extend Transport
|
|
10
|
+
|
|
11
|
+
# The fields Meta documents for a schedule. Meta's default set is every one of
|
|
12
|
+
# these except `id`, so ask for it explicitly when you need it. Offered for
|
|
13
|
+
# convenience, not enforcement — {.call} does not validate `fields` against this
|
|
14
|
+
# list, so a field Meta adds later works without a gem release.
|
|
15
|
+
module Fields
|
|
16
|
+
ID = "id"
|
|
17
|
+
NAME = "name"
|
|
18
|
+
DESCRIPTION = "description"
|
|
19
|
+
DELIVERY_TIME = "delivery_time"
|
|
20
|
+
STATUS = "status"
|
|
21
|
+
|
|
22
|
+
ALL = [ID, NAME, DESCRIPTION, DELIVERY_TIME, STATUS].freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# @example Read every field, including the id the default set leaves out
|
|
27
|
+
# List.call(fields: List::Fields::ALL)
|
|
28
|
+
# @example Fetch the next page
|
|
29
|
+
# page = List.call(limit: 50)
|
|
30
|
+
# List.call(limit: 50, after: page.next_cursor) if page.next_cursor
|
|
31
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
32
|
+
# @param fields [Array<String>, String, nil] Restrict each schedule to these
|
|
33
|
+
# fields; see {Fields::ALL}. Omitted when nil, so Meta returns its defaults.
|
|
34
|
+
# @param limit [Integer, nil] Schedules per page; Meta accepts 1 to 100.
|
|
35
|
+
# @param after [String, nil] Cursor for the page after this one.
|
|
36
|
+
# @param before [String, nil] Cursor for the page before this one.
|
|
37
|
+
# @return [Response::Collection]
|
|
38
|
+
# @raise [Error] if no WABA ID is configured, or the request fails.
|
|
39
|
+
def call(client: Client.new, fields: nil, limit: nil, after: nil, before: nil)
|
|
40
|
+
params = { fields: fields && Array(fields).join(","), limit:, after:, before: }.compact
|
|
41
|
+
response = client.connection.get(edge_path(client), params:)
|
|
42
|
+
|
|
43
|
+
Response::Collection.deserialize(
|
|
44
|
+
parse_json(handle_response!(response, error_class: Error, action: "list schedules"))
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
module Response
|
|
6
|
+
# A page of campaign schedules: `{data, paging}`.
|
|
7
|
+
#
|
|
8
|
+
# Enumerable over its schedules, so `list.select(&:scheduled?)` reads naturally
|
|
9
|
+
# without reaching for `.data` first.
|
|
10
|
+
#
|
|
11
|
+
# `paging` reuses {Whatsapp::MessageTemplates::Response::Paging}: the Graph API
|
|
12
|
+
# cursor shape is identical, and only the cursors are kept, not the
|
|
13
|
+
# `previous`/`next` URLs. As there, paging is exposed rather than followed
|
|
14
|
+
# automatically; pass {#next_cursor} back as `after:` to fetch the next page.
|
|
15
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
16
|
+
class Collection
|
|
17
|
+
include Enumerable
|
|
18
|
+
|
|
19
|
+
# @!attribute [rw] data
|
|
20
|
+
# @return [Array<Node>]
|
|
21
|
+
attr_accessor :data
|
|
22
|
+
|
|
23
|
+
# @!attribute [rw] paging
|
|
24
|
+
# @return [Whatsapp::MessageTemplates::Response::Paging, nil]
|
|
25
|
+
attr_accessor :paging
|
|
26
|
+
|
|
27
|
+
# @param data [Array<Node>]
|
|
28
|
+
# @param paging [Whatsapp::MessageTemplates::Response::Paging, nil]
|
|
29
|
+
def initialize(data: [], paging: nil)
|
|
30
|
+
@data = data
|
|
31
|
+
@paging = paging
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# @param response [Hash, nil] The parsed response body.
|
|
36
|
+
# @return [Collection]
|
|
37
|
+
def deserialize(response)
|
|
38
|
+
response ||= {}
|
|
39
|
+
|
|
40
|
+
new(
|
|
41
|
+
data: Array(response["data"]).map { |node| Node.deserialize(node) },
|
|
42
|
+
paging: response["paging"] && MessageTemplates::Response::Paging.deserialize(response["paging"])
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @yieldparam schedule [Node]
|
|
48
|
+
# @return [Enumerator]
|
|
49
|
+
def each(&)
|
|
50
|
+
data.each(&)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The cursor to pass as `after:` to fetch the next page.
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
def next_cursor
|
|
56
|
+
paging&.after
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
module Response
|
|
6
|
+
# The response to creating a campaign schedule: `{id}`.
|
|
7
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
8
|
+
class Created
|
|
9
|
+
# @!attribute [rw] id
|
|
10
|
+
# @return [String, nil] The new schedule's unique identifier.
|
|
11
|
+
attr_accessor :id
|
|
12
|
+
|
|
13
|
+
# @param id [String, nil]
|
|
14
|
+
def initialize(id: nil)
|
|
15
|
+
@id = id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# @param data [Hash, nil] The parsed response body.
|
|
20
|
+
# @return [Created]
|
|
21
|
+
def deserialize(data)
|
|
22
|
+
data ||= {}
|
|
23
|
+
|
|
24
|
+
new(id: data["id"])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
module Response
|
|
6
|
+
# One campaign schedule, as listed by {List}.
|
|
7
|
+
#
|
|
8
|
+
# Every attribute tolerates being absent: Meta returns only the fields asked for,
|
|
9
|
+
# and its default set omits `id`. `status` is kept as the raw string and never
|
|
10
|
+
# validated on the way in; compare it through the predicates or {Statuses}.
|
|
11
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
12
|
+
class Node
|
|
13
|
+
# @!attribute [rw] id
|
|
14
|
+
# @return [String, nil] The schedule's unique identifier.
|
|
15
|
+
attr_accessor :id
|
|
16
|
+
|
|
17
|
+
# @!attribute [rw] name
|
|
18
|
+
# @return [String, nil] The schedule's name.
|
|
19
|
+
attr_accessor :name
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] description
|
|
22
|
+
# @return [String, nil] The schedule's description.
|
|
23
|
+
attr_accessor :description
|
|
24
|
+
|
|
25
|
+
# @!attribute [rw] delivery_time
|
|
26
|
+
# @return [Integer, nil] The scheduled delivery time, as a Unix timestamp.
|
|
27
|
+
attr_accessor :delivery_time
|
|
28
|
+
|
|
29
|
+
# @!attribute [rw] status
|
|
30
|
+
# @return [String, nil] The raw status string Meta sent; {Statuses::ALL} lists the
|
|
31
|
+
# documented values, and anything else is kept as-is.
|
|
32
|
+
attr_accessor :status
|
|
33
|
+
|
|
34
|
+
# @param id [String, nil]
|
|
35
|
+
# @param name [String, nil]
|
|
36
|
+
# @param description [String, nil]
|
|
37
|
+
# @param delivery_time [Integer, nil]
|
|
38
|
+
# @param status [String, nil]
|
|
39
|
+
def initialize(id: nil, name: nil, description: nil, delivery_time: nil, status: nil)
|
|
40
|
+
@id = id
|
|
41
|
+
@name = name
|
|
42
|
+
@description = description
|
|
43
|
+
@delivery_time = delivery_time
|
|
44
|
+
@status = status
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# @param data [Hash, nil] One element of the `data` array.
|
|
49
|
+
# @return [Node]
|
|
50
|
+
def deserialize(data)
|
|
51
|
+
data ||= {}
|
|
52
|
+
|
|
53
|
+
new(
|
|
54
|
+
id: data["id"],
|
|
55
|
+
name: data["name"],
|
|
56
|
+
description: data["description"],
|
|
57
|
+
delivery_time: data["delivery_time"],
|
|
58
|
+
status: data["status"]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Boolean] Whether the schedule is still waiting for its delivery time.
|
|
64
|
+
def scheduled?
|
|
65
|
+
status == Statuses::SCHEDULED
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] Whether the campaign is being sent right now.
|
|
69
|
+
def sending?
|
|
70
|
+
status == Statuses::SENDING
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] Whether the campaign finished sending.
|
|
74
|
+
def completed?
|
|
75
|
+
status == Statuses::COMPLETED
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Boolean] Whether the campaign failed.
|
|
79
|
+
def failed?
|
|
80
|
+
status == Statuses::FAILED
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
# Deserializers for the responses of the `schedules` edge.
|
|
6
|
+
#
|
|
7
|
+
# Two response shapes, one per action, plus the element type of the list:
|
|
8
|
+
#
|
|
9
|
+
# {Collection} `{data, paging}` List
|
|
10
|
+
# {Created} `{id}` Create
|
|
11
|
+
# {Node} one element of `data`
|
|
12
|
+
#
|
|
13
|
+
# Every class follows the gem's `.deserialize(data)` convention and tolerates a nil
|
|
14
|
+
# or partial payload, so a field Meta stops sending cannot raise.
|
|
15
|
+
module Response
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
# The documented states of a campaign schedule.
|
|
6
|
+
#
|
|
7
|
+
# For comparison, not enforcement: {Response::Node#status} keeps whatever string
|
|
8
|
+
# Meta sends, so a status added later still round-trips.
|
|
9
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
10
|
+
module Statuses
|
|
11
|
+
COMPLETED = "COMPLETED"
|
|
12
|
+
FAILED = "FAILED"
|
|
13
|
+
SCHEDULED = "SCHEDULED"
|
|
14
|
+
SENDING = "SENDING"
|
|
15
|
+
|
|
16
|
+
ALL = [COMPLETED, FAILED, SCHEDULED, SENDING].freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
# The one piece shared by {List} and {Create}: both address the same WABA-scoped
|
|
6
|
+
# edge, differing only in HTTP verb and body. Extended (not included) because
|
|
7
|
+
# {#edge_path} is used from class-level `.call` methods (as a private class method).
|
|
8
|
+
#
|
|
9
|
+
# The guard itself and its message live in {Whatsapp::PathBuilding}, shared with every
|
|
10
|
+
# other ID-scoped feature; what stays here is the edge this module addresses.
|
|
11
|
+
module Transport
|
|
12
|
+
include Whatsapp::PathBuilding
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @param client [Whatsapp::Client]
|
|
17
|
+
# @return [String] The versioned path to the `schedules` edge.
|
|
18
|
+
# @raise [Error] if no WABA ID is configured.
|
|
19
|
+
def edge_path(client)
|
|
20
|
+
scoped_path(client, :waba_id, "schedules", error_class: Error, purpose: "schedules")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
# Lists and creates a WhatsApp Business Account's campaign schedules: an approved
|
|
5
|
+
# template sent to an audience at a set time.
|
|
6
|
+
#
|
|
7
|
+
# Schedules *send* templates that already exist; {Whatsapp::MessageTemplates} is what
|
|
8
|
+
# creates and manages the templates themselves. Addresses `waba_id`, not `phone_id`,
|
|
9
|
+
# like {Whatsapp::MessageTemplates} and {Whatsapp::SubscribedApp}.
|
|
10
|
+
# See `lib/ruby/whatsapp/schedule/CLAUDE.md` and `docs/schedule/README.md`.
|
|
11
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
12
|
+
module Schedule
|
|
13
|
+
class Error < Whatsapp::Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -5,18 +5,19 @@ module Whatsapp
|
|
|
5
5
|
# The one piece shared by {List}, {Subscribe}, and {Unsubscribe}: all three address
|
|
6
6
|
# the same WABA-scoped edge, differing only in HTTP verb and body. Extended (not
|
|
7
7
|
# included) since those classes are class-method-only.
|
|
8
|
+
#
|
|
9
|
+
# The guard itself and its message live in {Whatsapp::PathBuilding}, shared with every
|
|
10
|
+
# other ID-scoped feature; what stays here is the edge this module addresses.
|
|
8
11
|
module Transport
|
|
12
|
+
include Whatsapp::PathBuilding
|
|
13
|
+
|
|
9
14
|
private
|
|
10
15
|
|
|
11
16
|
# @param client [Whatsapp::Client]
|
|
12
17
|
# @return [String] The versioned path to the `subscribed_apps` edge.
|
|
13
18
|
# @raise [Error] if no WABA ID is configured.
|
|
14
19
|
def edge_path(client)
|
|
15
|
-
|
|
16
|
-
raise Error, "waba_id is required for subscribed apps; set it via Whatsapp.configure or Client.new(waba_id:)"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
client.path_for(client.waba_id, "subscribed_apps")
|
|
20
|
+
scoped_path(client, :waba_id, "subscribed_apps", error_class: Error, purpose: "subscribed apps")
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Webhook
|
|
5
|
+
# A business phone number's username status changed — approved, reserved, or deleted.
|
|
6
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
7
|
+
class BusinessUsernameUpdates
|
|
8
|
+
# @!attribute [rw] display_phone_number
|
|
9
|
+
# @return [String, nil]
|
|
10
|
+
attr_accessor :display_phone_number
|
|
11
|
+
|
|
12
|
+
# @!attribute [rw] username
|
|
13
|
+
# @return [String, nil] Omitted when `status` is `"deleted"`.
|
|
14
|
+
attr_accessor :username
|
|
15
|
+
|
|
16
|
+
# @!attribute [rw] status
|
|
17
|
+
# @return [String, nil] `"approved"`, `"reserved"`, or `"deleted"`.
|
|
18
|
+
attr_accessor :status
|
|
19
|
+
|
|
20
|
+
# @!attribute [r] raw
|
|
21
|
+
# @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part
|
|
22
|
+
# of the supported API: every attribute above is a flat top-level read, so a
|
|
23
|
+
# single renamed key yields an all-`nil` object with no other signal. Reach for
|
|
24
|
+
# it when a typed accessor is unexpectedly `nil`, log what you find, and treat
|
|
25
|
+
# it as read-only: it is the object `JSON.parse` returned, not a copy.
|
|
26
|
+
attr_reader :raw
|
|
27
|
+
|
|
28
|
+
def initialize(display_phone_number: nil, status: nil, username: nil, raw: {})
|
|
29
|
+
@display_phone_number = display_phone_number
|
|
30
|
+
@username = username
|
|
31
|
+
@status = status
|
|
32
|
+
@raw = raw
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# @param data [Hash] The raw `value` hash.
|
|
37
|
+
# @return [BusinessUsernameUpdates]
|
|
38
|
+
def deserialize(data)
|
|
39
|
+
data ||= {}
|
|
40
|
+
|
|
41
|
+
new(
|
|
42
|
+
display_phone_number: data["display_phone_number"],
|
|
43
|
+
username: data["username"],
|
|
44
|
+
status: data["status"],
|
|
45
|
+
raw: data
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Whatsapp
|
|
4
4
|
module Webhook
|
|
5
|
-
# A single `entry[].changes[]` item: `field` names which of Meta's
|
|
5
|
+
# A single `entry[].changes[]` item: `field` names which of Meta's 21 documented
|
|
6
6
|
# webhook notification types this is, `value` is the deserialized payload for it.
|
|
7
7
|
#
|
|
8
8
|
# `FIELDS` is populated incrementally as each field's value class is implemented
|
|
@@ -17,6 +17,7 @@ module Whatsapp
|
|
|
17
17
|
account_update: AccountUpdate,
|
|
18
18
|
automatic_events: AutomaticEvents,
|
|
19
19
|
business_capability_update: BusinessCapabilityUpdate,
|
|
20
|
+
business_username_updates: BusinessUsernameUpdates,
|
|
20
21
|
history: History,
|
|
21
22
|
message_template_components_update: MessageTemplateComponentsUpdate,
|
|
22
23
|
message_template_quality_update: MessageTemplateQualityUpdate,
|
|
@@ -29,6 +30,7 @@ module Whatsapp
|
|
|
29
30
|
smb_app_state_sync: SmbAppStateSync,
|
|
30
31
|
smb_message_echoes: SmbMessageEchoes,
|
|
31
32
|
template_category_update: TemplateCategoryUpdate,
|
|
33
|
+
user_id_update: UserIdUpdate,
|
|
32
34
|
user_preferences: UserPreferences,
|
|
33
35
|
}.freeze
|
|
34
36
|
|
|
@@ -10,13 +10,33 @@ module Whatsapp
|
|
|
10
10
|
# @return [String, nil]
|
|
11
11
|
attr_accessor :profile_name
|
|
12
12
|
|
|
13
|
+
# @!attribute [rw] username
|
|
14
|
+
# @return [String, nil] Set only if the user has adopted a username. Read from
|
|
15
|
+
# `profile.username`, where Meta's App Dashboard test payloads put it, falling
|
|
16
|
+
# back to a flat top-level `username` — `user_id`/`parent_user_id` arrive flat
|
|
17
|
+
# on this same hash and Meta publishes no reference schema for the contact
|
|
18
|
+
# object, so both locations are read rather than betting on one.
|
|
19
|
+
attr_accessor :username
|
|
20
|
+
|
|
13
21
|
# @!attribute [rw] wa_id
|
|
14
|
-
# @return [String, nil]
|
|
22
|
+
# @return [String, nil] Omitted once the user adopts a username, unless a
|
|
23
|
+
# recent interaction or the contact book still allows sharing the phone number.
|
|
15
24
|
attr_accessor :wa_id
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
# @!attribute [rw] user_id
|
|
27
|
+
# @return [String, nil] The user's business-scoped user ID (BSUID).
|
|
28
|
+
attr_accessor :user_id
|
|
29
|
+
|
|
30
|
+
# @!attribute [rw] parent_user_id
|
|
31
|
+
# @return [String, nil] The user's parent BSUID, present only when enrolled.
|
|
32
|
+
attr_accessor :parent_user_id
|
|
33
|
+
|
|
34
|
+
def initialize(profile_name: nil, username: nil, wa_id: nil, user_id: nil, parent_user_id: nil)
|
|
18
35
|
@profile_name = profile_name
|
|
36
|
+
@username = username
|
|
19
37
|
@wa_id = wa_id
|
|
38
|
+
@user_id = user_id
|
|
39
|
+
@parent_user_id = parent_user_id
|
|
20
40
|
end
|
|
21
41
|
|
|
22
42
|
class << self
|
|
@@ -27,7 +47,10 @@ module Whatsapp
|
|
|
27
47
|
|
|
28
48
|
new(
|
|
29
49
|
profile_name: data.dig("profile", "name"),
|
|
30
|
-
|
|
50
|
+
username: data.dig("profile", "username") || data["username"],
|
|
51
|
+
wa_id: data["wa_id"],
|
|
52
|
+
user_id: data["user_id"],
|
|
53
|
+
parent_user_id: data["parent_user_id"]
|
|
31
54
|
)
|
|
32
55
|
end
|
|
33
56
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Webhook
|
|
5
|
+
class History
|
|
6
|
+
# A single `history[]` entry: one chunk of a chat-history sync, plus the counters
|
|
7
|
+
# that tell you how far through the sync it is.
|
|
8
|
+
class Chunk
|
|
9
|
+
# @!attribute [rw] phase
|
|
10
|
+
# @return [Integer, nil] Which sync phase this chunk belongs to.
|
|
11
|
+
attr_accessor :phase
|
|
12
|
+
|
|
13
|
+
# @!attribute [rw] chunk_order
|
|
14
|
+
# @return [Integer, nil] This chunk's position within its phase.
|
|
15
|
+
attr_accessor :chunk_order
|
|
16
|
+
|
|
17
|
+
# @!attribute [rw] progress
|
|
18
|
+
# @return [Integer, nil] Percent complete, 0-100.
|
|
19
|
+
attr_accessor :progress
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] threads
|
|
22
|
+
# @return [Array<Thread>]
|
|
23
|
+
attr_accessor :threads
|
|
24
|
+
|
|
25
|
+
def initialize(phase: nil, chunk_order: nil, progress: nil, threads: [])
|
|
26
|
+
@phase = phase
|
|
27
|
+
@chunk_order = chunk_order
|
|
28
|
+
@progress = progress
|
|
29
|
+
@threads = threads
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# @param data [Hash, nil] A raw `history[]` entry.
|
|
34
|
+
# @return [Chunk]
|
|
35
|
+
def deserialize(data)
|
|
36
|
+
data ||= {}
|
|
37
|
+
metadata = data["metadata"] || {}
|
|
38
|
+
|
|
39
|
+
new(
|
|
40
|
+
phase: metadata["phase"],
|
|
41
|
+
chunk_order: metadata["chunk_order"],
|
|
42
|
+
progress: metadata["progress"],
|
|
43
|
+
threads: Array(data["threads"]).map { |thread_data| Thread.deserialize(thread_data) }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Webhook
|
|
5
|
+
class History
|
|
6
|
+
# One conversation within a history {Chunk}: who it is with, and the messages in it.
|
|
7
|
+
#
|
|
8
|
+
# Named for Meta's `threads[]` key. Nested under {History}, so it never shadows
|
|
9
|
+
# `::Thread` for anything outside this class.
|
|
10
|
+
class Thread
|
|
11
|
+
# @!attribute [rw] id
|
|
12
|
+
# @return [String, nil] The thread's identifier — the user's phone number, omitted
|
|
13
|
+
# if they had already adopted a username when the sync was requested and their
|
|
14
|
+
# number was no longer shareable. Use {#context} to identify them instead.
|
|
15
|
+
attr_accessor :id
|
|
16
|
+
|
|
17
|
+
# @!attribute [rw] context
|
|
18
|
+
# @return [Contact, nil] Who the thread is with (`wa_id`, `user_id`,
|
|
19
|
+
# `parent_user_id`, `username`). Deserialized with the same
|
|
20
|
+
# {Whatsapp::Webhook::Contact} as every other contact entry — its `username`
|
|
21
|
+
# read already falls back to a flat key, which is where Meta puts it here, and
|
|
22
|
+
# its `profile_name` is simply `nil` since a thread context carries no profile.
|
|
23
|
+
attr_accessor :context
|
|
24
|
+
|
|
25
|
+
# @!attribute [rw] messages
|
|
26
|
+
# @return [Array<Message::Base>] The synced messages, dispatched by `type` like
|
|
27
|
+
# any inbound message.
|
|
28
|
+
#
|
|
29
|
+
# Each one also carries a `history_context.status` (its delivery state at sync
|
|
30
|
+
# time), which is **not** modelled: it would put a history-only field on
|
|
31
|
+
# `Message::Base`, the envelope shared by every inbound message type, where it
|
|
32
|
+
# could never appear. Read it from {#raw} instead.
|
|
33
|
+
attr_accessor :messages
|
|
34
|
+
|
|
35
|
+
# @!attribute [r] raw
|
|
36
|
+
# @return [Hash] The untouched thread hash, so the unmodelled per-message
|
|
37
|
+
# `history_context` stays reachable. Read-only; not a copy.
|
|
38
|
+
attr_reader :raw
|
|
39
|
+
|
|
40
|
+
def initialize(id: nil, context: nil, messages: [], raw: {})
|
|
41
|
+
@id = id
|
|
42
|
+
@context = context
|
|
43
|
+
@messages = messages
|
|
44
|
+
@raw = raw
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# @param data [Hash, nil] A raw `threads[]` entry.
|
|
49
|
+
# @return [Thread]
|
|
50
|
+
def deserialize(data)
|
|
51
|
+
data ||= {}
|
|
52
|
+
|
|
53
|
+
new(
|
|
54
|
+
id: data["id"],
|
|
55
|
+
context: data["context"] ? Contact.deserialize(data["context"]) : nil,
|
|
56
|
+
messages: Array(data["messages"]).map { |message_data| Message.deserialize(message_data) },
|
|
57
|
+
raw: data
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|