ruby-whatsapp 0.4.2 → 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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +70 -7
  4. data/docs/README.md +4 -1
  5. data/docs/business_phone_number/README.md +12 -1
  6. data/docs/business_phone_number/contact_book.md +90 -0
  7. data/docs/business_phone_number/username.md +157 -0
  8. data/docs/configuration.md +6 -1
  9. data/docs/errors.md +12 -1
  10. data/docs/message_templates/README.md +1 -1
  11. data/docs/message_templates/components.md +37 -1
  12. data/docs/messages/README.md +86 -9
  13. data/docs/messages/address.md +1 -1
  14. data/docs/messages/audio.md +1 -1
  15. data/docs/messages/contacts.md +1 -1
  16. data/docs/messages/document.md +1 -1
  17. data/docs/messages/image.md +1 -1
  18. data/docs/messages/interactive.md +64 -3
  19. data/docs/messages/location.md +1 -1
  20. data/docs/messages/location_request.md +1 -1
  21. data/docs/messages/reaction.md +1 -1
  22. data/docs/messages/sticker.md +1 -1
  23. data/docs/messages/template.md +28 -1
  24. data/docs/messages/text.md +1 -1
  25. data/docs/messages/video.md +1 -1
  26. data/docs/schedule/README.md +183 -0
  27. data/docs/webhooks/README.md +20 -12
  28. data/docs/webhooks/business_username_updates.md +55 -0
  29. data/docs/webhooks/history.md +78 -15
  30. data/docs/webhooks/messages.md +141 -6
  31. data/docs/webhooks/smb_app_state_sync.md +50 -12
  32. data/docs/webhooks/smb_message_echoes.md +58 -21
  33. data/docs/webhooks/user_id_update.md +84 -0
  34. data/docs/webhooks/user_preferences.md +55 -16
  35. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  36. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  37. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  38. data/lib/ruby/whatsapp/business_phone_number/transport.rb +6 -5
  39. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  40. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  41. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  42. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  43. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  44. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  45. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  46. data/lib/ruby/whatsapp/business_phone_number.rb +9 -0
  47. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  48. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  49. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  50. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  51. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  52. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  53. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  54. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  55. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  56. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  57. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  58. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  59. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  60. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  61. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  62. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  63. data/lib/ruby/whatsapp/schedule.rb +15 -0
  64. data/lib/ruby/whatsapp/version.rb +1 -1
  65. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  66. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  67. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  68. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  69. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  70. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  71. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  72. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  73. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  74. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  75. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  76. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  77. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  78. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  79. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  80. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  81. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  82. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  83. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  84. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  85. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  86. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  87. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  88. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  89. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  90. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  91. metadata +38 -1
@@ -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
- def initialize(profile_name:, wa_id:)
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
- wa_id: data["wa_id"]
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
@@ -2,50 +2,89 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # Chat history synchronization for onboarded business customers.
5
+ # Chat history synchronization for onboarded business customers — one of the fields
6
+ # with a confirmed, JSON-example-backed schema, alongside {Messages},
7
+ # {UserIdUpdate}, {BusinessUsernameUpdates}, and {UserPreferences}.
6
8
  #
7
- # Best-effort schema: Meta's public docs describe this field in one line with
8
- # no published JSON example; `threads`' entry shape isn't confidently
9
- # typeable from that description alone, so it's kept as a raw array rather
10
- # than guessing further. Validate against a real payload before relying on
11
- # this in production.
12
- # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
+ # History arrives in chunks. Each `history[]` entry is one {Chunk}, carrying its own
10
+ # sync `metadata` (`phase`/`chunk_order`/`progress`) and the {Thread}s in it — note
11
+ # that the value-level `metadata` is the ordinary business-phone-number
12
+ # {Whatsapp::Webhook::Metadata}, a different object entirely.
13
+ #
14
+ # Meta sends a second variant of this field for a synced media asset, which carries
15
+ # `contacts` and `messages` at the value level instead of inside a thread; both are
16
+ # mapped here, and are empty for a thread-carrying payload.
17
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
13
18
  class History
14
- # @!attribute [rw] phase
15
- # @return [Integer, nil]
16
- attr_accessor :phase
17
-
18
- # @!attribute [rw] chunk_order
19
- # @return [Integer, nil]
20
- attr_accessor :chunk_order
21
-
22
- # @!attribute [rw] progress
23
- # @return [Integer, nil]
24
- attr_accessor :progress
25
-
26
- # @!attribute [rw] threads
27
- # @return [Array<Hash>]
28
- attr_accessor :threads
29
-
30
- def initialize(phase:, chunk_order:, progress:, threads:)
31
- @phase = phase
32
- @chunk_order = chunk_order
33
- @progress = progress
34
- @threads = threads
19
+ # @!attribute [rw] messaging_product
20
+ # @return [String, nil]
21
+ attr_accessor :messaging_product
22
+
23
+ # @!attribute [rw] metadata
24
+ # @return [Metadata, nil] The business phone number this history belongs to — not
25
+ # the per-chunk sync metadata, which lives on {Chunk}.
26
+ attr_accessor :metadata
27
+
28
+ # @!attribute [rw] chunks
29
+ # @return [Array<Chunk>]
30
+ attr_accessor :chunks
31
+
32
+ # @!attribute [rw] contacts
33
+ # @return [Array<Contact>] Populated only by the media-asset variant.
34
+ attr_accessor :contacts
35
+
36
+ # @!attribute [rw] messages
37
+ # @return [Array<Message::Base>] Populated only by the media-asset variant; a
38
+ # thread-carrying payload puts its messages on {Thread#messages} instead.
39
+ attr_accessor :messages
40
+
41
+ # @!attribute [r] raw
42
+ # @return [Hash] The untouched `value` hash. A diagnostic escape hatch, not part of
43
+ # the supported API: this field has two payload variants and the deeper one nests
44
+ # three levels, so a key that drifts would leave a typed accessor empty with no
45
+ # other trace. Treat it as read-only — it is the object `JSON.parse` returned.
46
+ attr_reader :raw
47
+
48
+ def initialize(messaging_product:, metadata:, chunks:, contacts:, messages:, raw: {})
49
+ @messaging_product = messaging_product
50
+ @metadata = metadata
51
+ @chunks = chunks
52
+ @contacts = contacts
53
+ @messages = messages
54
+ @raw = raw
55
+ end
56
+
57
+ # A delivery normally carries exactly one chunk, so these three read through to it
58
+ # rather than making every caller reach for `chunks.first`. Check {#chunks} directly
59
+ # if you need to handle a multi-chunk delivery.
60
+ # @return [Integer, nil]
61
+ def phase
62
+ chunks.first&.phase
63
+ end
64
+
65
+ # @return [Integer, nil]
66
+ def chunk_order
67
+ chunks.first&.chunk_order
68
+ end
69
+
70
+ # @return [Integer, nil] Percent complete, 0-100.
71
+ def progress
72
+ chunks.first&.progress
35
73
  end
36
74
 
37
75
  class << self
38
- # @param data [Hash] The raw `value` hash.
76
+ # @param data [Hash] The raw `history` field's `value` hash.
39
77
  # @return [History]
40
78
  def deserialize(data)
41
79
  data ||= {}
42
- metadata = data["metadata"] || {}
43
80
 
44
81
  new(
45
- phase: metadata["phase"],
46
- chunk_order: metadata["chunk_order"],
47
- progress: metadata["progress"],
48
- threads: Array(data["threads"])
82
+ messaging_product: data["messaging_product"],
83
+ metadata: data["metadata"] ? Metadata.deserialize(data["metadata"]) : nil,
84
+ chunks: Array(data["history"]).map { |chunk_data| Chunk.deserialize(chunk_data) },
85
+ contacts: Array(data["contacts"]).map { |contact_data| Contact.deserialize(contact_data) },
86
+ messages: Array(data["messages"]).map { |message_data| Message.deserialize(message_data) },
87
+ raw: data
49
88
  )
50
89
  end
51
90
  end
@@ -7,9 +7,24 @@ module Whatsapp
7
7
  # wamid, when it arrived, and (optionally) what it's replying to.
8
8
  class Base
9
9
  # @!attribute [rw] from
10
- # @return [String, nil]
10
+ # @return [String, nil] Omitted once the sender adopts a username, unless a
11
+ # recent interaction or the contact book still allows sharing the phone number.
11
12
  attr_accessor :from
12
13
 
14
+ # @!attribute [rw] from_user_id
15
+ # @return [String, nil] The sender's business-scoped user ID (BSUID).
16
+ attr_accessor :from_user_id
17
+
18
+ # @!attribute [rw] from_parent_user_id
19
+ # @return [String, nil] The sender's parent BSUID, present only when enrolled.
20
+ attr_accessor :from_parent_user_id
21
+
22
+ # @!attribute [rw] group_id
23
+ # @return [String, nil] Set only when the message was sent in a group chat.
24
+ # Reading groups works; this gem wraps no Groups API, so there is no way to
25
+ # reply into one — the id is for correlation and for filtering group traffic out.
26
+ attr_accessor :group_id
27
+
13
28
  # @!attribute [rw] id
14
29
  # @return [String, nil] The message's wamid.
15
30
  attr_accessor :id
@@ -30,8 +45,15 @@ module Whatsapp
30
45
  # @return [Referral, nil]
31
46
  attr_accessor :referral
32
47
 
33
- def initialize(from:, id:, timestamp:, type:, context: nil, referral: nil)
48
+ def initialize(
49
+ id:, timestamp:, type:,
50
+ from: nil, from_user_id: nil, from_parent_user_id: nil, group_id: nil,
51
+ context: nil, referral: nil
52
+ )
34
53
  @from = from
54
+ @from_user_id = from_user_id
55
+ @from_parent_user_id = from_parent_user_id
56
+ @group_id = group_id
35
57
  @id = id
36
58
  @timestamp = timestamp
37
59
  @type = type
@@ -47,6 +69,9 @@ module Whatsapp
47
69
  def common_attributes(data)
48
70
  {
49
71
  from: data["from"],
72
+ from_user_id: data["from_user_id"],
73
+ from_parent_user_id: data["from_parent_user_id"],
74
+ group_id: data["group_id"],
50
75
  id: data["id"],
51
76
  timestamp: data["timestamp"],
52
77
  type: data["type"],
@@ -34,7 +34,16 @@ module Whatsapp
34
34
  # @return [String, nil] Date in YYYY-MM-DD format.
35
35
  attr_accessor :birthday
36
36
 
37
- def initialize(name:, phones:, emails:, addresses:, urls:, org: nil, birthday: nil)
37
+ # @!attribute [rw] vcard
38
+ # @return [String, nil] The card's raw vCard, set only when `origin` is `"other"`.
39
+ attr_accessor :vcard
40
+
41
+ # @!attribute [rw] origin
42
+ # @return [String, nil] `"contact_request"` (a `REQUEST_CONTACT_INFO` button tap)
43
+ # or `"other"` (shared directly from the chat).
44
+ attr_accessor :origin
45
+
46
+ def initialize(name:, phones:, emails:, addresses:, urls:, org: nil, birthday: nil, vcard: nil, origin: nil)
38
47
  @name = name
39
48
  @phones = phones
40
49
  @emails = emails
@@ -42,6 +51,8 @@ module Whatsapp
42
51
  @org = org
43
52
  @urls = urls
44
53
  @birthday = birthday
54
+ @vcard = vcard
55
+ @origin = origin
45
56
  end
46
57
 
47
58
  class << self
@@ -57,7 +68,9 @@ module Whatsapp
57
68
  addresses: Array(data["addresses"]).map { |address_data| Address.deserialize(address_data) },
58
69
  org: data["org"] ? Org.deserialize(data["org"]) : nil,
59
70
  urls: Array(data["urls"]).map { |url_data| Url.deserialize(url_data) },
60
- birthday: data["birthday"]
71
+ birthday: data["birthday"],
72
+ vcard: data["vcard"],
73
+ origin: data["origin"]
61
74
  )
62
75
  end
63
76
  end
@@ -7,9 +7,22 @@ module Whatsapp
7
7
  # swipe, or a tap on an interactive button/list from an earlier message).
8
8
  class Context
9
9
  # @!attribute [rw] from
10
- # @return [String, nil]
10
+ # @return [String, nil] The WhatsApp ID of the quoted message's sender, absent
11
+ # once they adopt a username and their phone number is no longer shareable.
11
12
  attr_accessor :from
12
13
 
14
+ # @!attribute [rw] from_user_id
15
+ # @return [String, nil] The quoted sender's BSUID, the fallback identifier once
16
+ # {#from} is gone. **The key name is inferred** by symmetry with
17
+ # {Base#from_user_id} — Meta has not published a `context` example carrying
18
+ # it. Validate against a real payload before depending on it.
19
+ attr_accessor :from_user_id
20
+
21
+ # @!attribute [rw] from_parent_user_id
22
+ # @return [String, nil] The quoted sender's parent BSUID, present only for
23
+ # businesses enrolled in parent BSUIDs. Key name inferred, as above.
24
+ attr_accessor :from_parent_user_id
25
+
13
26
  # @!attribute [rw] id
14
27
  # @return [String, nil] The wamid of the message being replied to.
15
28
  attr_accessor :id
@@ -22,8 +35,14 @@ module Whatsapp
22
35
  # @return [Boolean, nil]
23
36
  attr_accessor :frequently_forwarded
24
37
 
25
- def initialize(from:, id:, forwarded: nil, frequently_forwarded: nil)
38
+ def initialize(
39
+ id:,
40
+ from: nil, from_user_id: nil, from_parent_user_id: nil,
41
+ forwarded: nil, frequently_forwarded: nil
42
+ )
26
43
  @from = from
44
+ @from_user_id = from_user_id
45
+ @from_parent_user_id = from_parent_user_id
27
46
  @id = id
28
47
  @forwarded = forwarded
29
48
  @frequently_forwarded = frequently_forwarded
@@ -37,6 +56,8 @@ module Whatsapp
37
56
 
38
57
  new(
39
58
  from: data["from"],
59
+ from_user_id: data["from_user_id"],
60
+ from_parent_user_id: data["from_parent_user_id"],
40
61
  id: data["id"],
41
62
  forwarded: data["forwarded"],
42
63
  frequently_forwarded: data["frequently_forwarded"]
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Message
6
+ # A user edited a message they had sent you. Like {Revoke}, a control message: it
7
+ # names the message being replaced and carries the replacement content.
8
+ #
9
+ # Also arrives through {Whatsapp::Webhook::SmbMessageEchoes} when the business
10
+ # itself edits a message from the WhatsApp Business app.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/edit
12
+ class Edit < Base
13
+ # @!attribute [rw] original_message_id
14
+ # @return [String, nil] The wamid of the message being replaced.
15
+ attr_accessor :original_message_id
16
+
17
+ # @!attribute [rw] message
18
+ # @return [Base, nil] The replacement content, dispatched by its own `type`
19
+ # through {Message.deserialize} like any other message.
20
+ #
21
+ # It carries *content only*. Meta's example gives the nested payload a `type`,
22
+ # its type body, and a `context`, but no `from`/`id`/`timestamp` of its own —
23
+ # those belong to the outer envelope, so the nested message's equivalents read
24
+ # `nil`. Reach for `edit.id` and `edit.from`, not `edit.message.id`.
25
+ attr_accessor :message
26
+
27
+ def initialize(original_message_id: nil, message: nil, **base_attributes)
28
+ super(**base_attributes)
29
+
30
+ @original_message_id = original_message_id
31
+ @message = message
32
+ end
33
+
34
+ class << self
35
+ # @param data [Hash] The raw message hash.
36
+ # @return [Edit]
37
+ def deserialize(data)
38
+ edit = data["edit"] || {}
39
+
40
+ new(
41
+ original_message_id: edit["original_message_id"],
42
+ message: edit["message"] ? Message.deserialize(edit["message"]) : nil,
43
+ **common_attributes(data)
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module Webhook
5
+ class Message
6
+ # A user deleted a message they had sent you. A control message rather than a
7
+ # content one: it names a message you already received instead of carrying a body.
8
+ #
9
+ # Also arrives through {Whatsapp::Webhook::SmbMessageEchoes} when the business
10
+ # itself revokes a message from the WhatsApp Business app.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/revoke
12
+ class Revoke < Base
13
+ # @!attribute [rw] original_message_id
14
+ # @return [String, nil] The wamid of the message that was deleted.
15
+ attr_accessor :original_message_id
16
+
17
+ def initialize(original_message_id: nil, **base_attributes)
18
+ super(**base_attributes)
19
+
20
+ @original_message_id = original_message_id
21
+ end
22
+
23
+ class << self
24
+ # @param data [Hash] The raw message hash.
25
+ # @return [Revoke]
26
+ def deserialize(data)
27
+ new(
28
+ original_message_id: data.dig("revoke", "original_message_id"),
29
+ **common_attributes(data)
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -17,16 +17,31 @@ module Whatsapp
17
17
  # @return [String, nil] The customer's (possibly new) WhatsApp ID.
18
18
  attr_accessor :wa_id
19
19
 
20
+ # @!attribute [rw] user_id
21
+ # @return [String, nil] The customer's new business-scoped user ID (BSUID),
22
+ # set when the change was a `user_changed_user_id` event.
23
+ attr_accessor :user_id
24
+
25
+ # @!attribute [rw] parent_user_id
26
+ # @return [String, nil] The customer's new parent BSUID, present only when enrolled.
27
+ attr_accessor :parent_user_id
28
+
20
29
  # @!attribute [rw] change_type
21
- # @return [String, nil] e.g. `"customer_changed_number"`, `"customer_identity_changed"`.
30
+ # @return [String, nil] e.g. `"customer_changed_number"`, `"user_changed_user_id"`.
22
31
  attr_accessor :change_type
23
32
 
24
- def initialize(body:, identity:, wa_id:, change_type:, **base_attributes)
33
+ def initialize(
34
+ change_type:,
35
+ body: nil, identity: nil, wa_id: nil, user_id: nil, parent_user_id: nil,
36
+ **base_attributes
37
+ )
25
38
  super(**base_attributes)
26
39
 
27
40
  @body = body
28
41
  @identity = identity
29
42
  @wa_id = wa_id
43
+ @user_id = user_id
44
+ @parent_user_id = parent_user_id
30
45
  @change_type = change_type
31
46
  end
32
47
 
@@ -38,6 +53,8 @@ module Whatsapp
38
53
  body: data.dig("system", "body"),
39
54
  identity: data.dig("system", "identity"),
40
55
  wa_id: data.dig("system", "wa_id"),
56
+ user_id: data.dig("system", "user_id"),
57
+ parent_user_id: data.dig("system", "parent_user_id"),
41
58
  change_type: data.dig("system", "type"),
42
59
  **common_attributes(data)
43
60
  )
@@ -7,9 +7,9 @@ module Whatsapp
7
7
  # type Meta added after this gem was released, or a message the Cloud API
8
8
  # itself couldn't process (carries an `errors[]` array in that case).
9
9
  class Unknown < Base
10
- # @!attribute [rw] raw
10
+ # @!attribute [r] raw
11
11
  # @return [Hash] The full, untyped raw message hash.
12
- attr_accessor :raw
12
+ attr_reader :raw
13
13
 
14
14
  # @!attribute [rw] errors
15
15
  # @return [Array<Error>]
@@ -22,6 +22,9 @@ module Whatsapp
22
22
  order: Order,
23
23
  system: System,
24
24
  reaction: Reaction,
25
+ # Control types: these name a prior message rather than carrying content of their own.
26
+ revoke: Revoke,
27
+ edit: Edit,
25
28
  }.freeze
26
29
 
27
30
  class << self
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Whatsapp
4
4
  module Webhook
5
- # The `messages` webhook field's `value` payload — the only field with a
6
- # confirmed, JSON-example-backed schema in Meta's docs. Carries inbound
7
- # messages and/or delivery status updates for one of your business phone numbers.
5
+ # The `messages` webhook field's `value` payload — one of five fields with a
6
+ # confirmed, JSON-example-backed schema in Meta's docs, alongside {UserIdUpdate}
7
+ # and {BusinessUsernameUpdates}. Carries inbound messages and/or delivery status
8
+ # updates for one of your business phone numbers.
8
9
  # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
9
10
  class Messages
10
11
  # @!attribute [rw] messaging_product