kapso-client-ruby 1.0.0 → 1.0.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/.rubocop.yml +81 -81
- data/CHANGELOG.md +262 -91
- data/Gemfile +20 -20
- data/RAILS_INTEGRATION.md +478 -0
- data/README.md +1053 -734
- data/Rakefile +40 -40
- data/TEMPLATE_TOOLS_GUIDE.md +120 -120
- data/WHATSAPP_24_HOUR_GUIDE.md +133 -133
- data/examples/advanced_features.rb +352 -349
- data/examples/advanced_messaging.rb +241 -0
- data/examples/basic_messaging.rb +139 -136
- data/examples/enhanced_interactive.rb +400 -0
- data/examples/flows_usage.rb +307 -0
- data/examples/interactive_messages.rb +343 -0
- data/examples/media_management.rb +256 -253
- data/examples/rails/jobs.rb +388 -0
- data/examples/rails/models.rb +240 -0
- data/examples/rails/notifications_controller.rb +227 -0
- data/examples/template_management.rb +393 -390
- data/kapso-ruby-logo.jpg +0 -0
- data/lib/kapso_client_ruby/client.rb +321 -316
- data/lib/kapso_client_ruby/errors.rb +348 -329
- data/lib/kapso_client_ruby/rails/generators/install_generator.rb +76 -0
- data/lib/kapso_client_ruby/rails/generators/templates/env.erb +21 -0
- data/lib/kapso_client_ruby/rails/generators/templates/initializer.rb.erb +33 -0
- data/lib/kapso_client_ruby/rails/generators/templates/message_service.rb.erb +138 -0
- data/lib/kapso_client_ruby/rails/generators/templates/webhook_controller.rb.erb +62 -0
- data/lib/kapso_client_ruby/rails/railtie.rb +55 -0
- data/lib/kapso_client_ruby/rails/service.rb +189 -0
- data/lib/kapso_client_ruby/rails/tasks.rake +167 -0
- data/lib/kapso_client_ruby/resources/calls.rb +172 -172
- data/lib/kapso_client_ruby/resources/contacts.rb +190 -190
- data/lib/kapso_client_ruby/resources/conversations.rb +103 -103
- data/lib/kapso_client_ruby/resources/flows.rb +382 -0
- data/lib/kapso_client_ruby/resources/media.rb +205 -205
- data/lib/kapso_client_ruby/resources/messages.rb +760 -380
- data/lib/kapso_client_ruby/resources/phone_numbers.rb +85 -85
- data/lib/kapso_client_ruby/resources/templates.rb +283 -283
- data/lib/kapso_client_ruby/types.rb +348 -262
- data/lib/kapso_client_ruby/version.rb +5 -5
- data/lib/kapso_client_ruby.rb +75 -68
- data/scripts/.env.example +17 -17
- data/scripts/kapso_template_finder.rb +91 -91
- data/scripts/sdk_setup.rb +404 -404
- data/scripts/test.rb +60 -60
- metadata +24 -3
|
@@ -1,381 +1,761 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module KapsoClientRuby
|
|
4
|
-
module Resources
|
|
5
|
-
class Messages
|
|
6
|
-
def initialize(client)
|
|
7
|
-
@client = client
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Text Messages
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
payload
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
interactive_obj
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KapsoClientRuby
|
|
4
|
+
module Resources
|
|
5
|
+
class Messages
|
|
6
|
+
def initialize(client)
|
|
7
|
+
@client = client
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Text Messages
|
|
11
|
+
# @param recipient_type [String] 'individual' or 'group' (default: 'individual')
|
|
12
|
+
def send_text(phone_number_id:, to:, body:, preview_url: nil, recipient_type: 'individual',
|
|
13
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
14
|
+
payload = build_base_payload(
|
|
15
|
+
phone_number_id: phone_number_id,
|
|
16
|
+
to: to,
|
|
17
|
+
type: 'text',
|
|
18
|
+
recipient_type: recipient_type,
|
|
19
|
+
context_message_id: context_message_id,
|
|
20
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
payload[:text] = { body: body }
|
|
24
|
+
payload[:text][:preview_url] = preview_url unless preview_url.nil?
|
|
25
|
+
|
|
26
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
27
|
+
body: payload.to_json, response_type: :json)
|
|
28
|
+
Types::SendMessageResponse.new(response)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Image Messages
|
|
32
|
+
# @param recipient_type [String] 'individual' or 'group' (default: 'individual')
|
|
33
|
+
def send_image(phone_number_id:, to:, image:, caption: nil, recipient_type: 'individual',
|
|
34
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
35
|
+
payload = build_base_payload(
|
|
36
|
+
phone_number_id: phone_number_id,
|
|
37
|
+
to: to,
|
|
38
|
+
type: 'image',
|
|
39
|
+
recipient_type: recipient_type,
|
|
40
|
+
context_message_id: context_message_id,
|
|
41
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
image_obj = build_media_object(image, caption)
|
|
45
|
+
payload[:image] = image_obj
|
|
46
|
+
|
|
47
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
48
|
+
body: payload.to_json, response_type: :json)
|
|
49
|
+
Types::SendMessageResponse.new(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Audio Messages
|
|
53
|
+
# @param phone_number_id [String] Phone number ID
|
|
54
|
+
# @param to [String] Recipient WhatsApp ID
|
|
55
|
+
# @param audio [Hash, String] Audio media (id or link)
|
|
56
|
+
# @param voice [Boolean] Set true for voice notes (OGG/OPUS format)
|
|
57
|
+
def send_audio(phone_number_id:, to:, audio:, voice: false,
|
|
58
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
59
|
+
payload = build_base_payload(
|
|
60
|
+
phone_number_id: phone_number_id,
|
|
61
|
+
to: to,
|
|
62
|
+
type: 'audio',
|
|
63
|
+
context_message_id: context_message_id,
|
|
64
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
audio_obj = build_media_object(audio)
|
|
68
|
+
|
|
69
|
+
# Add voice flag for voice notes (OGG/OPUS format recommended)
|
|
70
|
+
if voice
|
|
71
|
+
audio_obj[:voice] = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
payload[:audio] = audio_obj
|
|
75
|
+
|
|
76
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
77
|
+
body: payload.to_json, response_type: :json)
|
|
78
|
+
Types::SendMessageResponse.new(response)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Document Messages
|
|
82
|
+
def send_document(phone_number_id:, to:, document:, caption: nil, filename: nil,
|
|
83
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
84
|
+
payload = build_base_payload(
|
|
85
|
+
phone_number_id: phone_number_id,
|
|
86
|
+
to: to,
|
|
87
|
+
type: 'document',
|
|
88
|
+
context_message_id: context_message_id,
|
|
89
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
document_obj = build_media_object(document, caption)
|
|
93
|
+
document_obj[:filename] = filename if filename
|
|
94
|
+
payload[:document] = document_obj
|
|
95
|
+
|
|
96
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
97
|
+
body: payload.to_json, response_type: :json)
|
|
98
|
+
Types::SendMessageResponse.new(response)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Video Messages
|
|
102
|
+
# @param recipient_type [String] 'individual' or 'group' (default: 'individual')
|
|
103
|
+
def send_video(phone_number_id:, to:, video:, caption: nil, recipient_type: 'individual',
|
|
104
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
105
|
+
payload = build_base_payload(
|
|
106
|
+
phone_number_id: phone_number_id,
|
|
107
|
+
to: to,
|
|
108
|
+
type: 'video',
|
|
109
|
+
recipient_type: recipient_type,
|
|
110
|
+
context_message_id: context_message_id,
|
|
111
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
payload[:video] = build_media_object(video, caption)
|
|
115
|
+
|
|
116
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
117
|
+
body: payload.to_json, response_type: :json)
|
|
118
|
+
Types::SendMessageResponse.new(response)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Sticker Messages
|
|
122
|
+
def send_sticker(phone_number_id:, to:, sticker:, context_message_id: nil,
|
|
123
|
+
biz_opaque_callback_data: nil)
|
|
124
|
+
payload = build_base_payload(
|
|
125
|
+
phone_number_id: phone_number_id,
|
|
126
|
+
to: to,
|
|
127
|
+
type: 'sticker',
|
|
128
|
+
context_message_id: context_message_id,
|
|
129
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
payload[:sticker] = build_media_object(sticker)
|
|
133
|
+
|
|
134
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
135
|
+
body: payload.to_json, response_type: :json)
|
|
136
|
+
Types::SendMessageResponse.new(response)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Location Messages
|
|
140
|
+
def send_location(phone_number_id:, to:, latitude:, longitude:, name: nil,
|
|
141
|
+
address: nil, context_message_id: nil, biz_opaque_callback_data: nil)
|
|
142
|
+
payload = build_base_payload(
|
|
143
|
+
phone_number_id: phone_number_id,
|
|
144
|
+
to: to,
|
|
145
|
+
type: 'location',
|
|
146
|
+
context_message_id: context_message_id,
|
|
147
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
location_obj = {
|
|
151
|
+
latitude: latitude,
|
|
152
|
+
longitude: longitude
|
|
153
|
+
}
|
|
154
|
+
location_obj[:name] = name if name
|
|
155
|
+
location_obj[:address] = address if address
|
|
156
|
+
|
|
157
|
+
payload[:location] = location_obj
|
|
158
|
+
|
|
159
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
160
|
+
body: payload.to_json, response_type: :json)
|
|
161
|
+
Types::SendMessageResponse.new(response)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Contact Messages
|
|
165
|
+
def send_contacts(phone_number_id:, to:, contacts:, context_message_id: nil,
|
|
166
|
+
biz_opaque_callback_data: nil)
|
|
167
|
+
payload = build_base_payload(
|
|
168
|
+
phone_number_id: phone_number_id,
|
|
169
|
+
to: to,
|
|
170
|
+
type: 'contacts',
|
|
171
|
+
context_message_id: context_message_id,
|
|
172
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
payload[:contacts] = contacts
|
|
176
|
+
|
|
177
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
178
|
+
body: payload.to_json, response_type: :json)
|
|
179
|
+
Types::SendMessageResponse.new(response)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Template Messages
|
|
183
|
+
def send_template(phone_number_id:, to:, name:, language:, components: nil,
|
|
184
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
185
|
+
payload = build_base_payload(
|
|
186
|
+
phone_number_id: phone_number_id,
|
|
187
|
+
to: to,
|
|
188
|
+
type: 'template',
|
|
189
|
+
context_message_id: context_message_id,
|
|
190
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
template_obj = {
|
|
194
|
+
name: name,
|
|
195
|
+
language: { code: language }
|
|
196
|
+
}
|
|
197
|
+
template_obj[:components] = components if components
|
|
198
|
+
|
|
199
|
+
payload[:template] = template_obj
|
|
200
|
+
|
|
201
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
202
|
+
body: payload.to_json, response_type: :json)
|
|
203
|
+
Types::SendMessageResponse.new(response)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Reaction Messages
|
|
207
|
+
def send_reaction(phone_number_id:, to:, message_id:, emoji: nil,
|
|
208
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
209
|
+
payload = build_base_payload(
|
|
210
|
+
phone_number_id: phone_number_id,
|
|
211
|
+
to: to,
|
|
212
|
+
type: 'reaction',
|
|
213
|
+
context_message_id: context_message_id,
|
|
214
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
reaction_obj = { message_id: message_id }
|
|
218
|
+
reaction_obj[:emoji] = emoji if emoji # nil emoji removes reaction
|
|
219
|
+
|
|
220
|
+
payload[:reaction] = reaction_obj
|
|
221
|
+
|
|
222
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
223
|
+
body: payload.to_json, response_type: :json)
|
|
224
|
+
Types::SendMessageResponse.new(response)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Interactive Button Messages
|
|
228
|
+
# @param phone_number_id [String] Phone number ID
|
|
229
|
+
# @param to [String] Recipient WhatsApp ID
|
|
230
|
+
# @param body_text [String] Message body text
|
|
231
|
+
# @param buttons [Array<Hash>] Array of button objects (max 3)
|
|
232
|
+
# @param header [Hash, nil] Optional header (text, image, video, or document)
|
|
233
|
+
# @param footer [Hash, String, nil] Optional footer text or object
|
|
234
|
+
def send_interactive_buttons(phone_number_id:, to:, body_text:, buttons:,
|
|
235
|
+
header: nil, footer: nil, context_message_id: nil,
|
|
236
|
+
biz_opaque_callback_data: nil)
|
|
237
|
+
# Validate button count (max 3 buttons)
|
|
238
|
+
if buttons.length > 3
|
|
239
|
+
raise ArgumentError, "Maximum 3 buttons allowed (current: #{buttons.length})"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
if buttons.empty?
|
|
243
|
+
raise ArgumentError, 'At least 1 button is required'
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Validate header if provided (now supports text, image, video, document)
|
|
247
|
+
if header
|
|
248
|
+
validate_interactive_header(header, 'button')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
payload = build_base_payload(
|
|
252
|
+
phone_number_id: phone_number_id,
|
|
253
|
+
to: to,
|
|
254
|
+
type: 'interactive',
|
|
255
|
+
context_message_id: context_message_id,
|
|
256
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
interactive_obj = {
|
|
260
|
+
type: 'button',
|
|
261
|
+
body: { text: body_text },
|
|
262
|
+
action: { buttons: buttons }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Add header (supports text and media types)
|
|
266
|
+
interactive_obj[:header] = header if header
|
|
267
|
+
|
|
268
|
+
# Add footer (handle both string and hash formats)
|
|
269
|
+
if footer
|
|
270
|
+
interactive_obj[:footer] = footer.is_a?(String) ? { text: footer } : footer
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
payload[:interactive] = interactive_obj
|
|
274
|
+
|
|
275
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
276
|
+
body: payload.to_json, response_type: :json)
|
|
277
|
+
Types::SendMessageResponse.new(response)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Interactive List Messages
|
|
281
|
+
# @param phone_number_id [String] Phone number ID
|
|
282
|
+
# @param to [String] Recipient WhatsApp ID
|
|
283
|
+
# @param body_text [String] Message body text (max 4096 characters)
|
|
284
|
+
# @param button_text [String] Button text (list trigger)
|
|
285
|
+
# @param sections [Array<Hash>] List sections (max 10 rows total)
|
|
286
|
+
# @param header [Hash, nil] Optional text header only
|
|
287
|
+
# @param footer [Hash, String, nil] Optional footer text or object
|
|
288
|
+
def send_interactive_list(phone_number_id:, to:, body_text:, button_text:, sections:,
|
|
289
|
+
header: nil, footer: nil, context_message_id: nil,
|
|
290
|
+
biz_opaque_callback_data: nil)
|
|
291
|
+
# Validate body text length (updated to 4096)
|
|
292
|
+
if body_text.length > 4096
|
|
293
|
+
raise ArgumentError, "Body text max 4096 characters (current: #{body_text.length})"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Validate total row count (max 10 across all sections)
|
|
297
|
+
total_rows = sections.sum do |section|
|
|
298
|
+
rows = section[:rows] || section['rows'] || []
|
|
299
|
+
rows.length
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if total_rows > 10
|
|
303
|
+
raise ArgumentError, "Maximum 10 rows total across all sections (current: #{total_rows})"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if total_rows == 0
|
|
307
|
+
raise ArgumentError, 'At least 1 row is required'
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Header for lists must be text type only
|
|
311
|
+
if header
|
|
312
|
+
header_type = header[:type] || header['type']
|
|
313
|
+
unless header_type.nil? || header_type.to_s == 'text'
|
|
314
|
+
raise ArgumentError, "List messages only support text headers (received: #{header_type})"
|
|
315
|
+
end
|
|
316
|
+
validate_text_header(header) if header_type
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
payload = build_base_payload(
|
|
320
|
+
phone_number_id: phone_number_id,
|
|
321
|
+
to: to,
|
|
322
|
+
type: 'interactive',
|
|
323
|
+
context_message_id: context_message_id,
|
|
324
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
interactive_obj = {
|
|
328
|
+
type: 'list',
|
|
329
|
+
body: { text: body_text },
|
|
330
|
+
action: {
|
|
331
|
+
button: button_text,
|
|
332
|
+
sections: sections
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interactive_obj[:header] = header if header
|
|
337
|
+
|
|
338
|
+
# Add footer (handle both string and hash formats)
|
|
339
|
+
if footer
|
|
340
|
+
interactive_obj[:footer] = footer.is_a?(String) ? { text: footer } : footer
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
payload[:interactive] = interactive_obj
|
|
344
|
+
|
|
345
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
346
|
+
body: payload.to_json, response_type: :json)
|
|
347
|
+
Types::SendMessageResponse.new(response)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Send Flow Message
|
|
351
|
+
def send_flow(phone_number_id:, to:, flow_id:, flow_cta:, flow_token:,
|
|
352
|
+
screen: nil, flow_action: 'navigate', mode: 'published',
|
|
353
|
+
flow_action_payload: nil, header: nil, body_text: nil,
|
|
354
|
+
footer_text: nil, context_message_id: nil,
|
|
355
|
+
biz_opaque_callback_data: nil)
|
|
356
|
+
payload = build_base_payload(
|
|
357
|
+
phone_number_id: phone_number_id,
|
|
358
|
+
to: to,
|
|
359
|
+
type: 'interactive',
|
|
360
|
+
context_message_id: context_message_id,
|
|
361
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Build Flow action parameters
|
|
365
|
+
action_params = {
|
|
366
|
+
flow_message_version: '3',
|
|
367
|
+
flow_token: flow_token,
|
|
368
|
+
flow_id: flow_id,
|
|
369
|
+
flow_cta: flow_cta,
|
|
370
|
+
flow_action: flow_action,
|
|
371
|
+
mode: mode
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Add optional parameters
|
|
375
|
+
action_params[:flow_action_payload] = flow_action_payload if flow_action_payload
|
|
376
|
+
|
|
377
|
+
# Add screen parameter for navigate action
|
|
378
|
+
if flow_action == 'navigate' && screen
|
|
379
|
+
action_params[:flow_action_payload] ||= {}
|
|
380
|
+
action_params[:flow_action_payload][:screen] = screen
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
interactive_obj = {
|
|
384
|
+
type: 'flow',
|
|
385
|
+
action: action_params
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Add optional header and body
|
|
389
|
+
interactive_obj[:header] = header if header
|
|
390
|
+
interactive_obj[:body] = { text: body_text } if body_text
|
|
391
|
+
interactive_obj[:footer] = { text: footer_text } if footer_text
|
|
392
|
+
|
|
393
|
+
payload[:interactive] = interactive_obj
|
|
394
|
+
|
|
395
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
396
|
+
body: payload.to_json, response_type: :json)
|
|
397
|
+
Types::SendMessageResponse.new(response)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Send Interactive CTA URL Message
|
|
401
|
+
# @param phone_number_id [String] Phone number ID
|
|
402
|
+
# @param to [String] Recipient WhatsApp ID
|
|
403
|
+
# @param body_text [String] Message body text (max 1024 characters)
|
|
404
|
+
# @param display_text [String] Button display text (max 20 characters)
|
|
405
|
+
# @param url [String] Target URL (must be HTTPS)
|
|
406
|
+
# @param header [Hash, nil] Optional header (text, image, video, or document)
|
|
407
|
+
# @param footer_text [String, nil] Optional footer text (max 60 characters)
|
|
408
|
+
def send_interactive_cta_url(phone_number_id:, to:, body_text:, display_text:, url:,
|
|
409
|
+
header: nil, footer_text: nil, context_message_id: nil,
|
|
410
|
+
biz_opaque_callback_data: nil)
|
|
411
|
+
# Validate parameters
|
|
412
|
+
validate_cta_url_params(body_text, display_text, url, footer_text)
|
|
413
|
+
|
|
414
|
+
payload = build_base_payload(
|
|
415
|
+
phone_number_id: phone_number_id,
|
|
416
|
+
to: to,
|
|
417
|
+
type: 'interactive',
|
|
418
|
+
context_message_id: context_message_id,
|
|
419
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
interactive_obj = {
|
|
423
|
+
type: 'cta_url',
|
|
424
|
+
body: { text: body_text },
|
|
425
|
+
action: {
|
|
426
|
+
name: 'cta_url',
|
|
427
|
+
parameters: {
|
|
428
|
+
display_text: display_text,
|
|
429
|
+
url: url
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Add optional header (supports text, image, video, document)
|
|
435
|
+
if header
|
|
436
|
+
validate_interactive_header(header, 'cta_url')
|
|
437
|
+
interactive_obj[:header] = header
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Add optional footer
|
|
441
|
+
interactive_obj[:footer] = { text: footer_text } if footer_text
|
|
442
|
+
|
|
443
|
+
payload[:interactive] = interactive_obj
|
|
444
|
+
|
|
445
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
446
|
+
body: payload.to_json, response_type: :json)
|
|
447
|
+
Types::SendMessageResponse.new(response)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Send Interactive Catalog Message
|
|
451
|
+
# @param phone_number_id [String] Phone number ID
|
|
452
|
+
# @param to [String] Recipient WhatsApp ID
|
|
453
|
+
# @param body_text [String] Message body text (max 1024 characters)
|
|
454
|
+
# @param thumbnail_product_retailer_id [String] Product retailer ID for thumbnail
|
|
455
|
+
# @param footer_text [String, nil] Optional footer text (max 60 characters)
|
|
456
|
+
def send_interactive_catalog_message(phone_number_id:, to:, body_text:,
|
|
457
|
+
thumbnail_product_retailer_id:,
|
|
458
|
+
footer_text: nil, context_message_id: nil,
|
|
459
|
+
biz_opaque_callback_data: nil)
|
|
460
|
+
# Validate parameters
|
|
461
|
+
validate_catalog_message_params(body_text, thumbnail_product_retailer_id, footer_text)
|
|
462
|
+
|
|
463
|
+
payload = build_base_payload(
|
|
464
|
+
phone_number_id: phone_number_id,
|
|
465
|
+
to: to,
|
|
466
|
+
type: 'interactive',
|
|
467
|
+
context_message_id: context_message_id,
|
|
468
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
interactive_obj = {
|
|
472
|
+
type: 'catalog_message',
|
|
473
|
+
body: { text: body_text },
|
|
474
|
+
action: {
|
|
475
|
+
name: 'catalog_message',
|
|
476
|
+
parameters: {
|
|
477
|
+
thumbnail_product_retailer_id: thumbnail_product_retailer_id
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Add optional footer
|
|
483
|
+
interactive_obj[:footer] = { text: footer_text } if footer_text
|
|
484
|
+
|
|
485
|
+
payload[:interactive] = interactive_obj
|
|
486
|
+
|
|
487
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
488
|
+
body: payload.to_json, response_type: :json)
|
|
489
|
+
Types::SendMessageResponse.new(response)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Send Interactive Location Request
|
|
493
|
+
# @param phone_number_id [String] Phone number ID
|
|
494
|
+
# @param to [String] Recipient WhatsApp ID
|
|
495
|
+
# @param body_text [String] Message body text
|
|
496
|
+
# @param header [Hash, nil] Optional header (text, image, video, or document)
|
|
497
|
+
# @param footer_text [String, nil] Optional footer text
|
|
498
|
+
def send_interactive_location_request(phone_number_id:, to:, body_text:,
|
|
499
|
+
header: nil, footer_text: nil,
|
|
500
|
+
context_message_id: nil,
|
|
501
|
+
biz_opaque_callback_data: nil)
|
|
502
|
+
payload = build_base_payload(
|
|
503
|
+
phone_number_id: phone_number_id,
|
|
504
|
+
to: to,
|
|
505
|
+
type: 'interactive',
|
|
506
|
+
context_message_id: context_message_id,
|
|
507
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
interactive_obj = {
|
|
511
|
+
type: 'location_request_message',
|
|
512
|
+
body: { text: body_text },
|
|
513
|
+
action: {
|
|
514
|
+
name: 'send_location'
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# Add optional header (supports text, image, video, document)
|
|
519
|
+
if header
|
|
520
|
+
validate_interactive_header(header, 'location_request')
|
|
521
|
+
interactive_obj[:header] = header
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Add optional footer
|
|
525
|
+
interactive_obj[:footer] = { text: footer_text } if footer_text
|
|
526
|
+
|
|
527
|
+
payload[:interactive] = interactive_obj
|
|
528
|
+
|
|
529
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
530
|
+
body: payload.to_json, response_type: :json)
|
|
531
|
+
Types::SendMessageResponse.new(response)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Mark Message as Read
|
|
535
|
+
def mark_read(phone_number_id:, message_id:)
|
|
536
|
+
payload = {
|
|
537
|
+
messaging_product: 'whatsapp',
|
|
538
|
+
status: 'read',
|
|
539
|
+
message_id: message_id
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
543
|
+
body: payload.to_json, response_type: :json)
|
|
544
|
+
Types::GraphSuccessResponse.new(response)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Send Typing Indicator
|
|
548
|
+
def send_typing_indicator(phone_number_id:, to:)
|
|
549
|
+
payload = {
|
|
550
|
+
messaging_product: 'whatsapp',
|
|
551
|
+
recipient_type: 'individual',
|
|
552
|
+
to: to,
|
|
553
|
+
type: 'text',
|
|
554
|
+
text: { typing_indicator: { type: 'text' } }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
|
558
|
+
body: payload.to_json, response_type: :json)
|
|
559
|
+
Types::GraphSuccessResponse.new(response)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Query Message History (Kapso Proxy only)
|
|
563
|
+
def query(phone_number_id:, direction: nil, status: nil, since: nil, until_time: nil,
|
|
564
|
+
conversation_id: nil, limit: nil, after: nil, before: nil, fields: nil)
|
|
565
|
+
assert_kapso_proxy('Message history API')
|
|
566
|
+
|
|
567
|
+
query_params = {
|
|
568
|
+
phone_number_id: phone_number_id,
|
|
569
|
+
direction: direction,
|
|
570
|
+
status: status,
|
|
571
|
+
since: since,
|
|
572
|
+
until: until_time,
|
|
573
|
+
conversation_id: conversation_id,
|
|
574
|
+
limit: limit,
|
|
575
|
+
after: after,
|
|
576
|
+
before: before,
|
|
577
|
+
fields: fields
|
|
578
|
+
}.compact
|
|
579
|
+
|
|
580
|
+
response = @client.request(:get, "#{phone_number_id}/messages",
|
|
581
|
+
query: query_params, response_type: :json)
|
|
582
|
+
Types::PagedResponse.new(response)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# List Messages by Conversation (Kapso Proxy only)
|
|
586
|
+
def list_by_conversation(phone_number_id:, conversation_id:, limit: nil,
|
|
587
|
+
after: nil, before: nil, fields: nil)
|
|
588
|
+
query(
|
|
589
|
+
phone_number_id: phone_number_id,
|
|
590
|
+
conversation_id: conversation_id,
|
|
591
|
+
limit: limit,
|
|
592
|
+
after: after,
|
|
593
|
+
before: before,
|
|
594
|
+
fields: fields
|
|
595
|
+
)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
private
|
|
599
|
+
|
|
600
|
+
def build_base_payload(phone_number_id:, to:, type:, recipient_type: 'individual',
|
|
601
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
|
602
|
+
# Validate recipient_type
|
|
603
|
+
valid_types = ['individual', 'group']
|
|
604
|
+
unless valid_types.include?(recipient_type)
|
|
605
|
+
raise ArgumentError, "recipient_type must be 'individual' or 'group' (received: #{recipient_type})"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
payload = {
|
|
609
|
+
messaging_product: 'whatsapp',
|
|
610
|
+
recipient_type: recipient_type,
|
|
611
|
+
to: to,
|
|
612
|
+
type: type
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if context_message_id
|
|
616
|
+
payload[:context] = { message_id: context_message_id }
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
if biz_opaque_callback_data
|
|
620
|
+
payload[:biz_opaque_callback_data] = biz_opaque_callback_data
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
payload
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def build_media_object(media, caption = nil)
|
|
627
|
+
media_obj = case media
|
|
628
|
+
when Hash
|
|
629
|
+
media.dup
|
|
630
|
+
when String
|
|
631
|
+
# Assume it's either a media ID or URL
|
|
632
|
+
if media.match?(/\A\w+\z/) # Simple alphanumeric ID
|
|
633
|
+
{ id: media }
|
|
634
|
+
else
|
|
635
|
+
{ link: media }
|
|
636
|
+
end
|
|
637
|
+
else
|
|
638
|
+
raise ArgumentError, 'Media must be a Hash, media ID string, or URL string'
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
media_obj[:caption] = caption if caption
|
|
642
|
+
media_obj
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def assert_kapso_proxy(feature)
|
|
646
|
+
unless @client.kapso_proxy?
|
|
647
|
+
raise Errors::KapsoProxyRequiredError.new(feature)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Validate CTA URL parameters
|
|
652
|
+
def validate_cta_url_params(body_text, display_text, url, footer_text)
|
|
653
|
+
# Body text validation
|
|
654
|
+
if body_text.nil? || body_text.strip.empty?
|
|
655
|
+
raise ArgumentError, 'body_text is required'
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
if body_text.length > 1024
|
|
659
|
+
raise ArgumentError, "body_text max 1024 characters (current: #{body_text.length})"
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Display text validation
|
|
663
|
+
if display_text.nil? || display_text.strip.empty?
|
|
664
|
+
raise ArgumentError, 'display_text is required'
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
if display_text.length > 20
|
|
668
|
+
raise ArgumentError, "display_text max 20 characters (current: #{display_text.length})"
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# URL validation
|
|
672
|
+
if url.nil? || url.strip.empty?
|
|
673
|
+
raise ArgumentError, 'url is required'
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
unless url.match?(%r{\Ahttps?://}i)
|
|
677
|
+
raise ArgumentError, 'url must start with http:// or https://'
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Footer text validation
|
|
681
|
+
if footer_text && footer_text.length > 60
|
|
682
|
+
raise ArgumentError, "footer_text max 60 characters (current: #{footer_text.length})"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Validate catalog message parameters
|
|
687
|
+
def validate_catalog_message_params(body_text, thumbnail_product_retailer_id, footer_text)
|
|
688
|
+
# Body text validation
|
|
689
|
+
if body_text.nil? || body_text.strip.empty?
|
|
690
|
+
raise ArgumentError, 'body_text is required'
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
if body_text.length > 1024
|
|
694
|
+
raise ArgumentError, "body_text max 1024 characters (current: #{body_text.length})"
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Thumbnail product ID validation
|
|
698
|
+
if thumbnail_product_retailer_id.nil? || thumbnail_product_retailer_id.to_s.strip.empty?
|
|
699
|
+
raise ArgumentError, 'thumbnail_product_retailer_id is required'
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Footer text validation
|
|
703
|
+
if footer_text && footer_text.length > 60
|
|
704
|
+
raise ArgumentError, "footer_text max 60 characters (current: #{footer_text.length})"
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Validate interactive message header
|
|
709
|
+
def validate_interactive_header(header, message_type = 'interactive')
|
|
710
|
+
valid_types = ['text', 'image', 'video', 'document']
|
|
711
|
+
header_type = header[:type] || header['type']
|
|
712
|
+
|
|
713
|
+
if header_type.nil?
|
|
714
|
+
raise ArgumentError, 'Header must have a type field'
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
unless valid_types.include?(header_type.to_s)
|
|
718
|
+
raise ArgumentError, "Invalid header type '#{header_type}'. Must be one of: #{valid_types.join(', ')}"
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
case header_type.to_s
|
|
722
|
+
when 'text'
|
|
723
|
+
validate_text_header(header)
|
|
724
|
+
when 'image', 'video', 'document'
|
|
725
|
+
validate_media_header(header)
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Validate text header
|
|
730
|
+
def validate_text_header(header)
|
|
731
|
+
text = header[:text] || header['text']
|
|
732
|
+
|
|
733
|
+
if text.nil? || text.strip.empty?
|
|
734
|
+
raise ArgumentError, 'Text header requires text field'
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
if text.length > 60
|
|
738
|
+
raise ArgumentError, "Header text max 60 characters (current: #{text.length})"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Validate media header (image, video, document)
|
|
743
|
+
def validate_media_header(header)
|
|
744
|
+
header_type = (header[:type] || header['type']).to_s
|
|
745
|
+
media = header[header_type.to_sym] || header[header_type]
|
|
746
|
+
|
|
747
|
+
if media.nil?
|
|
748
|
+
raise ArgumentError, "#{header_type.capitalize} header requires #{header_type} field"
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Media must have id or link
|
|
752
|
+
has_id = media[:id] || media['id']
|
|
753
|
+
has_link = media[:link] || media['link']
|
|
754
|
+
|
|
755
|
+
unless has_id || has_link
|
|
756
|
+
raise ArgumentError, "#{header_type.capitalize} must have 'id' or 'link'"
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
end
|
|
381
761
|
end
|