ruby-whatsapp 0.4.2 → 0.6.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 +158 -0
- data/README.md +70 -7
- data/docs/README.md +4 -1
- data/docs/business_phone_number/README.md +12 -1
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +6 -1
- data/docs/errors.md +12 -1
- data/docs/message_templates/README.md +1 -1
- data/docs/message_templates/components.md +37 -1
- data/docs/messages/README.md +86 -9
- data/docs/messages/address.md +1 -1
- data/docs/messages/audio.md +1 -1
- data/docs/messages/contacts.md +1 -1
- data/docs/messages/document.md +1 -1
- data/docs/messages/image.md +1 -1
- data/docs/messages/interactive.md +84 -4
- data/docs/messages/location.md +1 -1
- data/docs/messages/location_request.md +1 -1
- data/docs/messages/reaction.md +1 -1
- data/docs/messages/sticker.md +1 -1
- data/docs/messages/template.md +28 -1
- data/docs/messages/text.md +1 -1
- data/docs/messages/video.md +1 -1
- data/docs/schedule/README.md +183 -0
- data/docs/webhooks/README.md +20 -12
- data/docs/webhooks/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +78 -15
- data/docs/webhooks/messages.md +141 -6
- data/docs/webhooks/smb_app_state_sync.md +50 -12
- data/docs/webhooks/smb_message_echoes.md +58 -21
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +55 -16
- 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/transport.rb +6 -5
- 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 +9 -0
- 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/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section.rb +11 -6
- data/lib/ruby/whatsapp/messages/interactive/list_buttons.rb +12 -0
- 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/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/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 +38 -1
|
@@ -14,21 +14,26 @@ module Whatsapp
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# @!attribute [rw] title
|
|
17
|
-
# @return [String]
|
|
17
|
+
# @return [String, nil]
|
|
18
18
|
attr_accessor :title
|
|
19
19
|
|
|
20
20
|
# @!attribute [rw] rows
|
|
21
21
|
# @return [Array<Hash>]
|
|
22
22
|
attr_accessor :rows
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
# Optional here: Meta requires a title only when the list has more than one
|
|
25
|
+
# section, which a lone Section cannot see — {ListButtons} enforces that rule.
|
|
26
|
+
validates :title, length: { maximum: Defaults::MAX_TITLE_LENGTH }, allow_nil: true
|
|
25
27
|
validates :rows, presence: true, length: { minimum: Defaults::MIN_ROWS_LENGTH, maximum: Defaults::MAX_ROWS_LENGTH }
|
|
26
28
|
|
|
27
|
-
#
|
|
29
|
+
# A blank title (`""` or whitespace) is treated as absent, so it is omitted from
|
|
30
|
+
# the payload rather than sent as an empty heading.
|
|
28
31
|
# @param rows [Array<Hash>] The rows in the section.
|
|
32
|
+
# @param title [String, nil] The title of the section. Required only when the list
|
|
33
|
+
# has more than one section (enforced by {ListButtons}).
|
|
29
34
|
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
30
|
-
def initialize(
|
|
31
|
-
@title = title
|
|
35
|
+
def initialize(rows:, title: nil)
|
|
36
|
+
@title = title.presence
|
|
32
37
|
@rows = rows
|
|
33
38
|
|
|
34
39
|
validate!
|
|
@@ -47,7 +52,7 @@ module Whatsapp
|
|
|
47
52
|
{
|
|
48
53
|
title:,
|
|
49
54
|
rows: serialized_rows,
|
|
50
|
-
}
|
|
55
|
+
}.compact
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
private
|
|
@@ -23,6 +23,7 @@ module Whatsapp
|
|
|
23
23
|
validates :button, presence: true, length: { maximum: Defaults::MAX_BUTTON_LENGTH }
|
|
24
24
|
validates :sections, presence: true, length: { minimum: Defaults::MIN_SECTIONS_COUNT,
|
|
25
25
|
maximum: Defaults::MAX_SECTIONS_COUNT, }
|
|
26
|
+
validate :validate_section_titles
|
|
26
27
|
|
|
27
28
|
# @param button [String] The text for the list button.
|
|
28
29
|
# @param sections [Array<Hash>] The sections of the list button.
|
|
@@ -52,6 +53,17 @@ module Whatsapp
|
|
|
52
53
|
|
|
53
54
|
private
|
|
54
55
|
|
|
56
|
+
# Meta requires `sections[].title` only once a list has more than one section, so
|
|
57
|
+
# {Section} leaves it optional and the rule lives here, where the count is known.
|
|
58
|
+
# A blank title counts as missing, matching {Section}, which omits it.
|
|
59
|
+
# @return [void]
|
|
60
|
+
def validate_section_titles
|
|
61
|
+
return if sections.blank? || sections.size <= 1
|
|
62
|
+
return if sections.all? { |section| section[:title].present? }
|
|
63
|
+
|
|
64
|
+
errors.add(:sections, "must each have a title when there is more than one section")
|
|
65
|
+
end
|
|
66
|
+
|
|
55
67
|
# @return [Array<Hash>] Serialized sections.
|
|
56
68
|
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
57
69
|
def serialized_sections
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
class Messages
|
|
5
|
+
class Interactive
|
|
6
|
+
# Asks the recipient to share their phone number, which they hand over with one tap.
|
|
7
|
+
#
|
|
8
|
+
# The interactive-message counterpart to
|
|
9
|
+
# {Whatsapp::MessageTemplates::Button::RequestContactInfo}: use this inside the
|
|
10
|
+
# 24-hour service window, and the template button to open a conversation. Either way
|
|
11
|
+
# this is how you get a phone number back from someone who has adopted a username and
|
|
12
|
+
# reaches you only as a
|
|
13
|
+
# [BSUID](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids).
|
|
14
|
+
#
|
|
15
|
+
# The only action kind with nothing for the caller to supply — the whole payload is
|
|
16
|
+
# `{ name: "request_contact_info" }` — which is why {Interactive} marks it
|
|
17
|
+
# parameterless and stops requiring an `action:`. Compare
|
|
18
|
+
# {Whatsapp::Messages::LocationRequest}, whose action is the same shape.
|
|
19
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
20
|
+
class RequestContactInfo < Base
|
|
21
|
+
module Defaults
|
|
22
|
+
ACTION_NAME = "request_contact_info"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @!attribute [rw] name
|
|
26
|
+
# @return [String] Always {Defaults::ACTION_NAME}.
|
|
27
|
+
attr_accessor :name
|
|
28
|
+
|
|
29
|
+
validates :name, inclusion: { in: [Defaults::ACTION_NAME] }
|
|
30
|
+
|
|
31
|
+
# @param name [String] The action name. Defaulted, since Meta accepts exactly one
|
|
32
|
+
# value; the keyword exists so a caller spelling out
|
|
33
|
+
# `action: { name: "request_contact_info" }` — mirroring Meta's example payload —
|
|
34
|
+
# still works.
|
|
35
|
+
# @raise [ActiveModel::ValidationError] if `name` is anything else.
|
|
36
|
+
def initialize(name: Defaults::ACTION_NAME)
|
|
37
|
+
@name = name
|
|
38
|
+
|
|
39
|
+
validate!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Takes a keyword splat rather than named parameters, because {Interactive} calls
|
|
44
|
+
# it as `serialize(**action)` with an empty hash for this kind.
|
|
45
|
+
# @return [Hash] Serialized representation of the request.
|
|
46
|
+
def serialize(**)
|
|
47
|
+
new(**).serialize
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Hash] Serialized representation of the request — no `parameters` key,
|
|
52
|
+
# unlike every other action kind, because there is nothing to parameterize.
|
|
53
|
+
def serialize
|
|
54
|
+
{ name: }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -19,6 +19,10 @@ module Whatsapp
|
|
|
19
19
|
url_button: { klass: UrlButton, api_type: "cta_url" },
|
|
20
20
|
media_carousel: { klass: MediaCarousel, api_type: "carousel" },
|
|
21
21
|
product_carousel: { klass: ProductCarousel, api_type: "product_list" },
|
|
22
|
+
# The one kind whose registry key and wire value coincide — the registry exists
|
|
23
|
+
# because they usually differ, so this is not a copy-paste slip. `parameterless`
|
|
24
|
+
# marks the one kind with nothing for a caller to supply; see #validate_action.
|
|
25
|
+
request_contact_info: { klass: RequestContactInfo, api_type: "request_contact_info", parameterless: true },
|
|
22
26
|
}.freeze
|
|
23
27
|
|
|
24
28
|
# @!attribute [rw] type
|
|
@@ -43,16 +47,17 @@ module Whatsapp
|
|
|
43
47
|
|
|
44
48
|
validates :type, presence: true, inclusion: { in: ACTION_TYPES.keys }
|
|
45
49
|
validates :body, presence: true
|
|
46
|
-
|
|
50
|
+
validate :validate_action
|
|
47
51
|
|
|
48
52
|
# @param type [Symbol, String] The action kind (see {ACTION_TYPES}).
|
|
49
53
|
# @param body [String] The body text of the interactive message.
|
|
50
|
-
# @param action [Hash] The action content, forwarded to the action class.
|
|
54
|
+
# @param action [Hash] The action content, forwarded to the action class. Defaults to
|
|
55
|
+
# empty, which only the parameterless `:request_contact_info` kind accepts.
|
|
51
56
|
# @param header [Hash, nil] The header content (optional).
|
|
52
57
|
# @param footer [Hash, nil] The footer content (optional).
|
|
53
|
-
# @param kwargs [Hash] Additional keyword arguments (
|
|
58
|
+
# @param kwargs [Hash] Additional keyword arguments passed to {Base} (:to / :recipient).
|
|
54
59
|
# @raise [ActiveModel::ValidationError] if validation fails.
|
|
55
|
-
def initialize(type:, body:, action
|
|
60
|
+
def initialize(type:, body:, action: {}, header: nil, footer: nil, **)
|
|
56
61
|
super(**)
|
|
57
62
|
|
|
58
63
|
@type = type.to_sym
|
|
@@ -87,6 +92,19 @@ module Whatsapp
|
|
|
87
92
|
ACTION_TYPES.fetch(type)
|
|
88
93
|
end
|
|
89
94
|
|
|
95
|
+
# An empty `action` is blank, so a plain presence validator would reject the one kind
|
|
96
|
+
# that legitimately has nothing to supply. Reads the registry with `[]` rather than
|
|
97
|
+
# the `fetch`-backed #action_config, so an unknown `type` falls through to the
|
|
98
|
+
# inclusion validator above instead of raising KeyError from inside a validator.
|
|
99
|
+
# @return [void]
|
|
100
|
+
def validate_action
|
|
101
|
+
config = ACTION_TYPES[type]
|
|
102
|
+
return if config.nil? || config[:parameterless]
|
|
103
|
+
return if action.present?
|
|
104
|
+
|
|
105
|
+
errors.add(:action, "can't be blank")
|
|
106
|
+
end
|
|
107
|
+
|
|
90
108
|
# @return [Hash, nil] The serialized header information.
|
|
91
109
|
def serialized_header
|
|
92
110
|
return unless header
|
|
@@ -5,18 +5,34 @@ module Whatsapp
|
|
|
5
5
|
class Response
|
|
6
6
|
class Contacts
|
|
7
7
|
# @!attribute [rw] input
|
|
8
|
-
# @return [String]
|
|
8
|
+
# @return [String] Whatever you addressed the message to: the phone number, the
|
|
9
|
+
# BSUID or parent BSUID, or a group ID.
|
|
9
10
|
attr_accessor :input
|
|
10
11
|
|
|
11
12
|
# @!attribute [rw] wa_id
|
|
12
|
-
# @return [String]
|
|
13
|
+
# @return [String, nil] The recipient's WhatsApp ID. Present only if the message
|
|
14
|
+
# was sent to their phone number; omitted otherwise.
|
|
13
15
|
attr_accessor :wa_id
|
|
14
16
|
|
|
17
|
+
# @!attribute [rw] user_id
|
|
18
|
+
# @return [String, nil] The recipient's business-scoped user ID (BSUID), or parent
|
|
19
|
+
# BSUID — Meta returns either in this one field, unlike the webhook classes,
|
|
20
|
+
# which separate `user_id` from `parent_user_id`.
|
|
21
|
+
#
|
|
22
|
+
# Omitted whenever the message went to a phone number, **including when both
|
|
23
|
+
# `to` and `recipient` were supplied**, since Meta gives `to` precedence and
|
|
24
|
+
# answers as though `recipient` had not been sent. A `nil` here therefore does
|
|
25
|
+
# not mean the recipient has no BSUID.
|
|
26
|
+
attr_accessor :user_id
|
|
27
|
+
|
|
15
28
|
# @param input [String] The input value of the contact.
|
|
16
|
-
# @param wa_id [String] The WhatsApp ID of the contact
|
|
17
|
-
|
|
29
|
+
# @param wa_id [String, nil] The WhatsApp ID of the contact, if it was addressed by
|
|
30
|
+
# phone number.
|
|
31
|
+
# @param user_id [String, nil] The contact's BSUID, if it was addressed by one.
|
|
32
|
+
def initialize(input:, wa_id: nil, user_id: nil)
|
|
18
33
|
@input = input
|
|
19
34
|
@wa_id = wa_id
|
|
35
|
+
@user_id = user_id
|
|
20
36
|
end
|
|
21
37
|
|
|
22
38
|
class << self
|
|
@@ -26,7 +42,8 @@ module Whatsapp
|
|
|
26
42
|
def deserialize(data)
|
|
27
43
|
new(
|
|
28
44
|
input: data["input"],
|
|
29
|
-
wa_id: data["wa_id"]
|
|
45
|
+
wa_id: data["wa_id"],
|
|
46
|
+
user_id: data["user_id"]
|
|
30
47
|
)
|
|
31
48
|
end
|
|
32
49
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whatsapp
|
|
4
|
+
module Schedule
|
|
5
|
+
# Schedules an approved template to be sent to an audience at a set time.
|
|
6
|
+
#
|
|
7
|
+
# A validated instance rather than a bare class method (unlike {List}, which has
|
|
8
|
+
# nothing to check): Meta documents all six body fields as required, so a missing
|
|
9
|
+
# one fails here rather than costing a request. Nothing beyond that is checked —
|
|
10
|
+
# Meta publishes no length limits and no rule that `delivery_time` must be in the
|
|
11
|
+
# future.
|
|
12
|
+
# Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api
|
|
13
|
+
class Create
|
|
14
|
+
include ActiveModel::Validations
|
|
15
|
+
extend ResponseHandling
|
|
16
|
+
extend Transport
|
|
17
|
+
|
|
18
|
+
# @!attribute [rw] hsm_id
|
|
19
|
+
# @return [String] The ID of the approved template to send.
|
|
20
|
+
attr_accessor :hsm_id
|
|
21
|
+
|
|
22
|
+
# @!attribute [rw] audience_id
|
|
23
|
+
# @return [String] The ID of the audience to send it to.
|
|
24
|
+
attr_accessor :audience_id
|
|
25
|
+
|
|
26
|
+
# @!attribute [rw] waba_cs_id
|
|
27
|
+
# @return [String] What Meta documents only as "WABA customer status ID (phone
|
|
28
|
+
# number)"; whether that means a phone number ID or the number itself is
|
|
29
|
+
# unverified, so it is passed through as given.
|
|
30
|
+
attr_accessor :waba_cs_id
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] name
|
|
33
|
+
# @return [String] The schedule's name.
|
|
34
|
+
attr_accessor :name
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] description
|
|
37
|
+
# @return [String] The schedule's description.
|
|
38
|
+
attr_accessor :description
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] delivery_time
|
|
41
|
+
# @return [Integer] When to send, as a Unix timestamp.
|
|
42
|
+
attr_accessor :delivery_time
|
|
43
|
+
|
|
44
|
+
validates :hsm_id, :audience_id, :waba_cs_id, :name, :description, presence: true
|
|
45
|
+
validates :delivery_time, numericality: { only_integer: true }
|
|
46
|
+
|
|
47
|
+
# @param hsm_id [String] See {#hsm_id}.
|
|
48
|
+
# @param audience_id [String] See {#audience_id}.
|
|
49
|
+
# @param waba_cs_id [String] See {#waba_cs_id}.
|
|
50
|
+
# @param name [String] See {#name}.
|
|
51
|
+
# @param description [String] See {#description}.
|
|
52
|
+
# @param delivery_time [Integer] A Unix timestamp; pass `time.to_i` for a `Time`.
|
|
53
|
+
# @raise [ActiveModel::ValidationError] if a field is blank, or `delivery_time`
|
|
54
|
+
# is not an integer.
|
|
55
|
+
def initialize(hsm_id:, audience_id:, waba_cs_id:, name:, description:, delivery_time:)
|
|
56
|
+
@hsm_id = hsm_id
|
|
57
|
+
@audience_id = audience_id
|
|
58
|
+
@waba_cs_id = waba_cs_id
|
|
59
|
+
@name = name
|
|
60
|
+
@description = description
|
|
61
|
+
@delivery_time = delivery_time
|
|
62
|
+
|
|
63
|
+
validate!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Hash] The schedule payload. Every field is documented required, so
|
|
67
|
+
# none is ever compacted away.
|
|
68
|
+
def serialize
|
|
69
|
+
{ hsm_id:, audience_id:, waba_cs_id:, name:, description:, delivery_time: }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
# @example Send a template to an audience in one hour
|
|
74
|
+
# Create.call(
|
|
75
|
+
# hsm_id: "1234567890", audience_id: "2345678901", waba_cs_id: "3456789012",
|
|
76
|
+
# name: "Black Friday", description: "Black Friday promo to VIP customers",
|
|
77
|
+
# delivery_time: Time.now.to_i + 3600
|
|
78
|
+
# ).id
|
|
79
|
+
# @param hsm_id [String] See {#initialize}.
|
|
80
|
+
# @param audience_id [String] See {#initialize}.
|
|
81
|
+
# @param waba_cs_id [String] See {#initialize}.
|
|
82
|
+
# @param name [String] See {#initialize}.
|
|
83
|
+
# @param description [String] See {#initialize}.
|
|
84
|
+
# @param delivery_time [Integer] See {#initialize}.
|
|
85
|
+
# @param client [Whatsapp::Client] The WhatsApp client instance.
|
|
86
|
+
# @return [Response::Created]
|
|
87
|
+
# @raise [ActiveModel::ValidationError] if a field is blank, or `delivery_time`
|
|
88
|
+
# is not an integer.
|
|
89
|
+
# @raise [Error] if no WABA ID is configured, or the request fails.
|
|
90
|
+
def call(hsm_id:, audience_id:, waba_cs_id:, name:, description:, delivery_time:, client: Client.new)
|
|
91
|
+
body = new(hsm_id:, audience_id:, waba_cs_id:, name:, description:, delivery_time:).serialize
|
|
92
|
+
response = client.connection.post(edge_path(client), json: body)
|
|
93
|
+
|
|
94
|
+
Response::Created.deserialize(
|
|
95
|
+
parse_json(handle_response!(response, error_class: Error, action: "create schedule"))
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -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
|