whatsapp_sdk 1.1.0 → 1.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a1704465f5fbbe203905c5d63ac14944838fb6617ffb24f7ca6727dba2cdd49
4
- data.tar.gz: 2359c3c077eab75ac9281a70134744eaf7703e765a4932ac6fa677287a143669
3
+ metadata.gz: 6e1bc509c72a21009421ece7489cc79d178abec74db4445d84cf579fe374d5d3
4
+ data.tar.gz: b547704f8654b49e389547d27014089fdf1583583e57c6f567bf1193dbfc72aa
5
5
  SHA512:
6
- metadata.gz: 067f3899b14284af61f426b1cf7f04d1e5fda2d3abb135a51bdac9fe1060427b270a2c4a2796855ed222c96a8794513377878b7bed56021bed2fe81eafc7c93c
7
- data.tar.gz: 1789fd6e89f858e0a60996d462af11fcad70dfd48cf4bf6c76bacf736be725f3cf255b72d6cd731f959bf1715becac6a4fb4668f08c020e0004ae60aefda9db1
6
+ metadata.gz: f302ce6f5126585be265c8a0bdcc7c7681cd27e6838ce6c961a0b196eb66ba0cc3522c87407cbc69af7d95f8adeca02f7dbcf76ee646fddff49e3e50ba6c62b3
7
+ data.tar.gz: d7a97fa6bc6de65711c1d1698cd38863650dfc7f2dd0ef5ec743d2c938e15bca6511f6481c5fd72ee2972ec258466c9066817a9a3ea378f98c0d2db26a05e186
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Unreleased
2
2
 
3
+ # v 1.2.0
4
+ - Expose Graph media upload sessions through the configured client @oitedi [199](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/199)
5
+ - Support BSUID recipients and preserve user IDs in message responses @oitedi [198](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/198)
6
+ - Add voice message support to audio sends @oitedi [197](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/197)
7
+ - Guard `GenericErrorResponse.build_from_response` against non-Hash input @afonso-aguas [195](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/195)
8
+ - Fix false-positive `HttpResponseError` on bodies containing "error" substring @afonso-aguas [194](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/194)
9
+
3
10
  # v 1.1.0
4
11
  - Add support for pagination in template_analytics with 'after' cursor @osvaldo-santos [190](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/190)
5
12
  - Add template_analytics feature @osvaldo-santos [189](https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/pull/189)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- whatsapp_sdk (1.1.0)
4
+ whatsapp_sdk (1.2.0)
5
5
  faraday (~> 2.0, > 2.0.1)
6
6
  faraday-multipart (~> 1)
7
7
  zeitwerk (~> 2)
@@ -9,8 +9,8 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- addressable (2.8.1)
13
- public_suffix (>= 2.0.2, < 6.0)
12
+ addressable (2.9.0)
13
+ public_suffix (>= 2.0.2, < 8.0)
14
14
  ast (2.4.2)
15
15
  base64 (0.2.0)
16
16
  coderay (1.1.3)
@@ -25,10 +25,10 @@ GEM
25
25
  faraday-net_http (3.4.2)
26
26
  net-http (~> 0.5)
27
27
  hashdiff (1.0.1)
28
- json (2.18.1)
28
+ json (2.19.9)
29
29
  logger (1.7.0)
30
30
  method_source (1.0.0)
31
- minitest (5.16.1)
31
+ minitest (5.27.0)
32
32
  mocha (1.14.0)
33
33
  multipart-post (2.4.1)
34
34
  net-http (0.9.1)
@@ -41,7 +41,7 @@ GEM
41
41
  method_source (~> 1.0)
42
42
  pry-nav (1.0.0)
43
43
  pry (>= 0.9.10, < 0.15)
44
- public_suffix (5.0.0)
44
+ public_suffix (5.1.1)
45
45
  rainbow (3.1.1)
46
46
  rake (12.3.3)
47
47
  regexp_parser (2.5.0)
@@ -78,6 +78,7 @@ PLATFORMS
78
78
  arm64-darwin-22
79
79
  arm64-darwin-23
80
80
  arm64-darwin-24
81
+ arm64-darwin-25
81
82
  x86_64-darwin-19
82
83
  x86_64-darwin-21
83
84
  x86_64-linux
data/README.md CHANGED
@@ -535,6 +535,54 @@ If the API response is still `success`, but the message is not delivered:
535
535
 
536
536
  Note: Sometimes the messages are delayed; see [Meta documentation](https://developers.facebook.com/docs/whatsapp/on-premises/guides/send-message-performance#delays).
537
537
 
538
+
539
+ ### Voice messages
540
+
541
+ Pass `voice: true` to `send_audio` with an uploaded `audio_id` or hosted `link`.
542
+ Voice messages require an Ogg file encoded with Opus; the SDK does not transcode media.
543
+ Omitting `voice` (or passing `false`) sends basic audio.
544
+
545
+ ```ruby
546
+ client.messages.send_audio(sender_id: sender_id, recipient_number: recipient_number,
547
+ audio_id: media_id, voice: true)
548
+ ```
549
+
550
+ See [Meta's audio message documentation](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/audio-messages/).
551
+
552
+ ### Upload handles for templates and profile photos
553
+
554
+ Use Graph upload sessions when an endpoint requires a media handle instead of a WhatsApp media ID:
555
+
556
+ ```ruby
557
+ session = client.media.create_upload_session(app_id: app_id, file_path: "header.png", type: "image/png")
558
+ upload = client.media.upload_file_to_session(session_id: session.fetch("id"), file_path: "header.png")
559
+ handle = upload.fetch("h")
560
+ ```
561
+
562
+ Both methods use the client's configured API version and token. An optional `access_token:`
563
+ overrides the token for that request only. They return the Graph response hashes and raise
564
+ `WhatsappSdk::Api::Responses::HttpResponseError` on Graph errors, like the other APIs.
565
+ Missing files or directories raise `WhatsappSdk::Api::Medias::FileNotFoundError`.
566
+ The upload reads the entire file into memory and starts at offset zero; automatic retries and
567
+ resuming interrupted uploads are not included.
568
+ See [Meta's upload guide](https://developers.facebook.com/docs/graph-api/guides/upload).
569
+
570
+ ### Business-scoped user IDs (BSUIDs)
571
+
572
+ All message sends accept `recipient:` as an alternative to `recipient_number:`.
573
+ Use the complete BSUID (or parent BSUID), including its country prefix:
574
+
575
+ ```ruby
576
+ response = client.messages.send_text(sender_id: sender_id, recipient: "US.123abc", message: "Hello")
577
+ response.contacts.first.user_id # => "US.123abc"
578
+ response.contacts.first.wa_id # => nil when Meta omits the phone number
579
+ ```
580
+
581
+ Phone numbers take precedence when both destinations are supplied. Missing or blank
582
+ selected destinations raise `WhatsappSdk::Resource::Errors::MissingArgumentError` before sending.
583
+ Authentication templates require a phone number; the SDK does not infer template category from its name or buttons.
584
+ See [Meta's BSUID documentation](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids/).
585
+
538
586
  ## Development
539
587
 
540
588
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
@@ -7,7 +7,7 @@ module WhatsappSdk
7
7
  module Api
8
8
  class Client
9
9
  API_VERSIONS = [
10
- 'v24.0', 'v23.0', 'v22.0', 'v21.0', 'v20.0', 'v19.0', 'v18.0', 'v17.0', 'v16.0', 'v15.0', 'v14.0',
10
+ 'v25.0', 'v24.0', 'v23.0', 'v22.0', 'v21.0', 'v20.0', 'v19.0', 'v18.0', 'v17.0', 'v16.0', 'v15.0', 'v14.0',
11
11
  'v13.0', 'v12.0', 'v11.0', 'v10.0', 'v9.0', 'v8.0', 'v7.0', 'v6.0', 'v5.0', 'v4.0',
12
12
  'v3.3', 'v3.2', 'v3.1', 'v3.0', 'v2.12', 'v2.11', 'v2.10', 'v2.9', 'v2.8', 'v2.7',
13
13
  'v2.6', 'v2.5', 'v2.4', 'v2.3', 'v2.2', 'v2.1'
@@ -58,13 +58,13 @@ module WhatsappSdk
58
58
 
59
59
  response = faraday_request.public_send(http_method, endpoint, request_params(params, headers), headers)
60
60
 
61
- if response.status > 499 || Api::Responses::GenericErrorResponse.response_error?(response: response.body)
62
- raise Api::Responses::HttpResponseError.new(http_status: response.status, body: JSON.parse(response.body))
63
- end
61
+ parsed_body = parse_response_body(response.body)
64
62
 
65
- return nil if response.body == ""
63
+ if response.status > 499 || Api::Responses::GenericErrorResponse.response_error?(response: parsed_body)
64
+ raise Api::Responses::HttpResponseError.new(http_status: response.status, body: parsed_body)
65
+ end
66
66
 
67
- JSON.parse(response.body)
67
+ parsed_body
68
68
  end
69
69
 
70
70
  def download_file(url:, content_type_header:, file_path: nil)
@@ -85,6 +85,12 @@ module WhatsappSdk
85
85
 
86
86
  private
87
87
 
88
+ def parse_response_body(body)
89
+ return nil if body.nil? || body.empty?
90
+
91
+ JSON.parse(body)
92
+ end
93
+
88
94
  def request_params(params, headers)
89
95
  return params.to_json if params.is_a?(Hash) && headers['Content-Type'] == 'application/json'
90
96
 
@@ -117,6 +117,33 @@ module WhatsappSdk
117
117
  Api::Responses::SuccessResponse.success_response?(response: response)
118
118
  end
119
119
 
120
+ # Create a Graph upload session for template media or a business profile photo.
121
+ # Returns the raw response containing the upload session "id".
122
+ # Uses the client's API version and token unless access_token is supplied.
123
+ def create_upload_session(app_id:, file_path:, type:, access_token: nil)
124
+ raise FileNotFoundError.new(file_path: file_path) unless File.file?(file_path)
125
+
126
+ headers = { 'Content-Type' => 'application/json' }
127
+ headers['Authorization'] = "Bearer #{access_token}" if access_token
128
+ send_request(
129
+ endpoint: "./#{app_id}/uploads",
130
+ params: { file_name: File.basename(file_path), file_length: File.size(file_path), file_type: type },
131
+ headers: headers
132
+ )
133
+ end
134
+
135
+ # Upload the complete file to a Graph upload session, starting at offset zero.
136
+ # Returns the raw response containing the reusable media handle "h".
137
+ # This does not resume partial uploads or retry failed requests.
138
+ def upload_file_to_session(session_id:, file_path:, access_token: nil)
139
+ raise FileNotFoundError.new(file_path: file_path) unless File.file?(file_path)
140
+
141
+ headers = { 'Content-Type' => 'application/octet-stream', 'file_offset' => '0' }
142
+ headers['Authorization'] = "Bearer #{access_token}" if access_token
143
+ # ponytail: buffers the whole file; stream bytes if large uploads become a requirement.
144
+ send_request(endpoint: "./#{session_id}", params: File.binread(file_path), headers: headers)
145
+ end
146
+
120
147
  private
121
148
 
122
149
  def map_media_type_to_content_type_header(media_type)
@@ -13,8 +13,9 @@ module WhatsappSdk
13
13
  # @param recipient_number [Integer] Recipient' Phone number.
14
14
  # @param message [String] Text to send.
15
15
  # @param message_id [String] The id of the message to reply to.
16
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
16
17
  # @return [MessageDataResponse] Response object.
17
- def send_text(sender_id:, recipient_number:, message:, message_id: nil)
18
+ def send_text(sender_id:, message:, recipient_number: nil, message_id: nil, recipient: nil)
18
19
  params = {
19
20
  messaging_product: "whatsapp",
20
21
  to: recipient_number,
@@ -23,6 +24,7 @@ module WhatsappSdk
23
24
  text: { body: message }
24
25
  }
25
26
  params[:context] = { message_id: message_id } if message_id
27
+ apply_recipient!(params, recipient_number, recipient)
26
28
 
27
29
  response = send_request(
28
30
  endpoint: endpoint(sender_id),
@@ -42,9 +44,10 @@ module WhatsappSdk
42
44
  # @param name [String] Location name.
43
45
  # @param address [String] Location address.
44
46
  # @param message_id [String] The id of the message to reply to.
47
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
45
48
  # @return [MessageDataResponse] Response object.
46
49
  def send_location(
47
- sender_id:, recipient_number:, longitude:, latitude:, name:, address:, message_id: nil
50
+ sender_id:, longitude:, latitude:, name:, address:, recipient_number: nil, message_id: nil, recipient: nil
48
51
  )
49
52
  params = {
50
53
  messaging_product: "whatsapp",
@@ -59,6 +62,7 @@ module WhatsappSdk
59
62
  }
60
63
  }
61
64
  params[:context] = { message_id: message_id } if message_id
65
+ apply_recipient!(params, recipient_number, recipient)
62
66
 
63
67
  response = send_request(
64
68
  endpoint: endpoint(sender_id),
@@ -77,9 +81,10 @@ module WhatsappSdk
77
81
  # @param link [String] Image link.
78
82
  # @param caption [String] Image caption.
79
83
  # @param message_id [String] The id of the message to reply to.
84
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
80
85
  # @return [MessageDataResponse] Response object.
81
86
  def send_image(
82
- sender_id:, recipient_number:, image_id: nil, link: nil, caption: "", message_id: nil
87
+ sender_id:, recipient_number: nil, image_id: nil, link: nil, caption: "", message_id: nil, recipient: nil
83
88
  )
84
89
  raise Resource::Errors::MissingArgumentError, "image_id or link is required" if !image_id && !link
85
90
 
@@ -95,6 +100,7 @@ module WhatsappSdk
95
100
  { id: image_id, caption: caption }
96
101
  end
97
102
  params[:context] = { message_id: message_id } if message_id
103
+ apply_recipient!(params, recipient_number, recipient)
98
104
 
99
105
  response = send_request(
100
106
  endpoint: endpoint(sender_id),
@@ -112,8 +118,12 @@ module WhatsappSdk
112
118
  # @param audio_id [String] Audio ID.
113
119
  # @param link [String] Audio link.
114
120
  # @param message_id [String] The id of the message to reply to.
121
+ # @param voice [Boolean] Send an Ogg/Opus voice message instead of basic audio. Defaults to false.
122
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
115
123
  # @return [MessageDataResponse] Response object.
116
- def send_audio(sender_id:, recipient_number:, audio_id: nil, link: nil, message_id: nil)
124
+ def send_audio(
125
+ sender_id:, recipient_number: nil, audio_id: nil, link: nil, message_id: nil, voice: false, recipient: nil
126
+ )
117
127
  raise Resource::Errors::MissingArgumentError, "audio_id or link is required" if !audio_id && !link
118
128
 
119
129
  params = {
@@ -122,8 +132,9 @@ module WhatsappSdk
122
132
  recipient_type: "individual",
123
133
  type: "audio"
124
134
  }
125
- params[:audio] = link ? { link: link } : { id: audio_id }
135
+ params[:audio] = link ? { link: link, voice: voice } : { id: audio_id, voice: voice }
126
136
  params[:context] = { message_id: message_id } if message_id
137
+ apply_recipient!(params, recipient_number, recipient)
127
138
 
128
139
  response = send_request(
129
140
  endpoint: endpoint(sender_id),
@@ -143,9 +154,10 @@ module WhatsappSdk
143
154
  # @param link [String] Image link.
144
155
  # @param caption [String] Image caption.
145
156
  # @param message_id [String] The id of the message to reply to.
157
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
146
158
  # @return [MessageDataResponse] Response object.
147
159
  def send_video(
148
- sender_id:, recipient_number:, video_id: nil, link: nil, caption: "", message_id: nil
160
+ sender_id:, recipient_number: nil, video_id: nil, link: nil, caption: "", message_id: nil, recipient: nil
149
161
  )
150
162
  raise Resource::Errors::MissingArgumentError, "video_id or link is required" if !video_id && !link
151
163
 
@@ -161,6 +173,7 @@ module WhatsappSdk
161
173
  { id: video_id, caption: caption }
162
174
  end
163
175
  params[:context] = { message_id: message_id } if message_id
176
+ apply_recipient!(params, recipient_number, recipient)
164
177
 
165
178
  response = send_request(
166
179
  endpoint: endpoint(sender_id),
@@ -179,9 +192,11 @@ module WhatsappSdk
179
192
  # @param link [String] Image link.
180
193
  # @param caption [String] Image caption.
181
194
  # @param message_id [String] The id of the message to reply to.
195
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
182
196
  # @return [MessageDataResponse] Response object.
183
197
  def send_document(
184
- sender_id:, recipient_number:, document_id: nil, link: nil, caption: "", message_id: nil, filename: nil
198
+ sender_id:, recipient_number: nil, document_id: nil, link: nil, caption: "", message_id: nil,
199
+ filename: nil, recipient: nil
185
200
  )
186
201
  if !document_id && !link
187
202
  raise Resource::Errors::MissingArgumentError,
@@ -201,6 +216,7 @@ module WhatsappSdk
201
216
  end
202
217
  params[:document] = params[:document].merge({ filename: filename }) if filename
203
218
  params[:context] = { message_id: message_id } if message_id
219
+ apply_recipient!(params, recipient_number, recipient)
204
220
 
205
221
  response = send_request(
206
222
  endpoint: endpoint(sender_id),
@@ -211,15 +227,16 @@ module WhatsappSdk
211
227
  Api::Responses::MessageDataResponse.build_from_response(response: response)
212
228
  end
213
229
 
214
- # Send a document.
230
+ # Send a sticker.
215
231
  #
216
232
  # @param sender_id [Integer] Sender' phone number.
217
233
  # @param recipient_number [Integer] Recipient' Phone number.
218
234
  # @param sticker_id [String] The sticker ID.
219
235
  # @param link [String] Image link.
220
236
  # @param message_id [String] The id of the message to reply to.
237
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
221
238
  # @return [MessageDataResponse] Response object.
222
- def send_sticker(sender_id:, recipient_number:, sticker_id: nil, link: nil, message_id: nil)
239
+ def send_sticker(sender_id:, recipient_number: nil, sticker_id: nil, link: nil, message_id: nil, recipient: nil)
223
240
  raise Resource::Errors::MissingArgumentError, "sticker or link is required" if !sticker_id && !link
224
241
 
225
242
  params = {
@@ -230,6 +247,7 @@ module WhatsappSdk
230
247
  }
231
248
  params[:sticker] = link ? { link: link } : { id: sticker_id }
232
249
  params[:context] = { message_id: message_id } if message_id
250
+ apply_recipient!(params, recipient_number, recipient)
233
251
 
234
252
  response = send_request(
235
253
  endpoint: endpoint(sender_id),
@@ -248,9 +266,10 @@ module WhatsappSdk
248
266
  # @param contacts [Array<Contact>] Contacts.
249
267
  # @param contacts_json [Json] Contacts.
250
268
  # @param message_id [String] The id of the message to reply to.
269
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
251
270
  # @return [MessageDataResponse] Response object.
252
271
  def send_contacts(
253
- sender_id:, recipient_number:, contacts: nil, contacts_json: {}, message_id: nil
272
+ sender_id:, recipient_number: nil, contacts: nil, contacts_json: {}, message_id: nil, recipient: nil
254
273
  )
255
274
  params = {
256
275
  messaging_product: "whatsapp",
@@ -260,6 +279,7 @@ module WhatsappSdk
260
279
  }
261
280
  params[:contacts] = contacts ? contacts.map(&:to_h) : contacts_json
262
281
  params[:context] = { message_id: message_id } if message_id
282
+ apply_recipient!(params, recipient_number, recipient)
263
283
 
264
284
  response = send_request(
265
285
  endpoint: endpoint(sender_id),
@@ -284,9 +304,10 @@ module WhatsappSdk
284
304
  # @param interactive_json [Json] The interactive object as a Json.
285
305
  # If you pass interactive_json, you can't pass interactive.
286
306
  # @param message_id [String] The id of the message to reply to.
307
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
287
308
  # @return [MessageDataResponse] Response object.
288
309
  def send_interactive_message(
289
- sender_id:, recipient_number:, interactive: nil, interactive_json: nil, message_id: nil
310
+ sender_id:, recipient_number: nil, interactive: nil, interactive_json: nil, message_id: nil, recipient: nil
290
311
  )
291
312
  if !interactive && !interactive_json
292
313
  raise Resource::Errors::MissingArgumentError,
@@ -306,6 +327,7 @@ module WhatsappSdk
306
327
  interactive.to_json
307
328
  end
308
329
  params[:context] = { message_id: message_id } if message_id
330
+ apply_recipient!(params, recipient_number, recipient)
309
331
 
310
332
  response = send_request(
311
333
  endpoint: endpoint(sender_id),
@@ -348,9 +370,11 @@ module WhatsappSdk
348
370
  # @param language [String] template language.
349
371
  # @param components [Component] Component.
350
372
  # @param components_json [Json] The component as a Json. If you pass components_json, you can't pass components.
373
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
374
+ # Not allowed for authentication templates.
351
375
  # @return [MessageDataResponse] Response object.
352
376
  def send_template(
353
- sender_id:, recipient_number:, name:, language:, components: nil, components_json: nil
377
+ sender_id:, name:, language:, recipient_number: nil, components: nil, components_json: nil, recipient: nil
354
378
  )
355
379
  if !components && !components_json
356
380
  raise Resource::Errors::MissingArgumentError,
@@ -373,6 +397,7 @@ module WhatsappSdk
373
397
  else
374
398
  components.map(&:to_json)
375
399
  end
400
+ apply_recipient!(params, recipient_number, recipient)
376
401
 
377
402
  response = send_request(
378
403
  endpoint: endpoint(sender_id),
@@ -389,8 +414,9 @@ module WhatsappSdk
389
414
  # @param recipient_number [Integer] Recipient' Phone number.
390
415
  # @param message_id [String] the id of the message to reaction.
391
416
  # @param emoji [String] unicode of the emoji to send.
417
+ # @param recipient [String] Recipient' Business-Scoped User ID (BSUID). Optional alternative to recipient_number.
392
418
  # @return [MessageDataResponse] Response object.
393
- def send_reaction(sender_id:, recipient_number:, message_id:, emoji:)
419
+ def send_reaction(sender_id:, message_id:, emoji:, recipient_number: nil, recipient: nil)
394
420
  params = {
395
421
  messaging_product: "whatsapp",
396
422
  recipient_type: "individual",
@@ -401,6 +427,7 @@ module WhatsappSdk
401
427
  emoji: emoji
402
428
  }
403
429
  }
430
+ apply_recipient!(params, recipient_number, recipient)
404
431
 
405
432
  response = send_request(
406
433
  endpoint: endpoint(sender_id),
@@ -441,6 +468,22 @@ module WhatsappSdk
441
468
  def endpoint(sender_id)
442
469
  "#{sender_id}/messages"
443
470
  end
471
+
472
+ # Applies the destination to the payload.
473
+ # - Phone send (recipient_number present): keeps `to`, never adds `recipient` — payload is
474
+ # identical to the pre-BSUID behavior.
475
+ # - BSUID send (recipient_number absent, recipient present): drops `to` and sets `recipient`
476
+ # (the field Meta uses to address a Business-Scoped User ID).
477
+ # Requires at least one destination.
478
+ def apply_recipient!(params, recipient_number, recipient)
479
+ if (recipient_number.nil? ? recipient : recipient_number).to_s.strip.empty?
480
+ raise Resource::Errors::MissingArgumentError, "recipient_number or recipient is required"
481
+ end
482
+
483
+ params.delete(:to) if recipient_number.nil?
484
+ params[:recipient] = recipient if recipient_number.nil? && recipient
485
+ params
486
+ end
444
487
  end
445
488
  end
446
489
  end
@@ -16,10 +16,14 @@ module WhatsappSdk
16
16
  end
17
17
 
18
18
  def self.response_error?(response:)
19
+ return false unless response.is_a?(Hash)
20
+
19
21
  response["error"]
20
22
  end
21
23
 
22
24
  def self.build_from_response(response:)
25
+ return unless response.is_a?(Hash)
26
+
23
27
  error_response = response["error"]
24
28
  return unless error_response
25
29
 
@@ -27,7 +27,9 @@ module WhatsappSdk
27
27
  end
28
28
 
29
29
  def parse_contact(contact_json)
30
- Resource::ContactResponse.new(input: contact_json["input"], wa_id: contact_json["wa_id"])
30
+ Resource::ContactResponse.new(
31
+ input: contact_json["input"], wa_id: contact_json["wa_id"], user_id: contact_json["user_id"]
32
+ )
31
33
  end
32
34
  end
33
35
  end
@@ -3,11 +3,12 @@
3
3
  module WhatsappSdk
4
4
  module Resource
5
5
  class ContactResponse
6
- attr_accessor :wa_id, :input
6
+ attr_accessor :wa_id, :input, :user_id
7
7
 
8
- def initialize(input:, wa_id:)
8
+ def initialize(input:, wa_id: nil, user_id: nil)
9
9
  @input = input
10
10
  @wa_id = wa_id
11
+ @user_id = user_id
11
12
  end
12
13
  end
13
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WhatsappSdk
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whatsapp_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ignacio-chiazzo
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-02-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -196,7 +195,6 @@ metadata:
196
195
  source_code_uri: https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk
197
196
  changelog_uri: https://github.com/ignacio-chiazzo/ruby_whatsapp_sdk/blob/main/CHANGELOG.md
198
197
  rubygems_mfa_required: 'true'
199
- post_install_message:
200
198
  rdoc_options: []
201
199
  require_paths:
202
200
  - lib
@@ -211,8 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
209
  - !ruby/object:Gem::Version
212
210
  version: '0'
213
211
  requirements: []
214
- rubygems_version: 3.4.1
215
- signing_key:
212
+ rubygems_version: 4.0.20
216
213
  specification_version: 4
217
214
  summary: Use the Ruby Whatsapp SDK to communicate with Whatsapp API using the Cloud
218
215
  API