flow_chat 0.4.0 → 0.4.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/Gemfile +1 -0
- data/README.md +408 -102
- data/examples/initializer.rb +1 -1
- data/examples/media_prompts_examples.rb +27 -0
- data/examples/multi_tenant_whatsapp_controller.rb +60 -64
- data/examples/ussd_controller.rb +17 -11
- data/examples/whatsapp_controller.rb +11 -12
- data/examples/whatsapp_media_examples.rb +404 -0
- data/examples/whatsapp_message_job.rb +111 -0
- data/lib/flow_chat/base_processor.rb +8 -4
- data/lib/flow_chat/config.rb +37 -0
- data/lib/flow_chat/session/cache_session_store.rb +5 -5
- data/lib/flow_chat/simulator/controller.rb +78 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +1982 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +1 -2
- data/lib/flow_chat/ussd/prompt.rb +39 -5
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +8 -2
- data/lib/flow_chat/whatsapp/client.rb +435 -0
- data/lib/flow_chat/whatsapp/configuration.rb +50 -12
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +113 -115
- data/lib/flow_chat/whatsapp/middleware/executor.rb +1 -1
- data/lib/flow_chat/whatsapp/processor.rb +1 -11
- data/lib/flow_chat/whatsapp/prompt.rb +125 -84
- data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +7 -7
- metadata +8 -3
- data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
- data/lib/flow_chat/ussd/simulator/views/simulator.html.erb +0 -239
@@ -17,7 +17,6 @@ module FlowChat
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def configure_middleware_stack(builder)
|
20
|
-
builder.use gateway
|
21
20
|
builder.use FlowChat::Session::Middleware
|
22
21
|
builder.use FlowChat::Ussd::Middleware::Pagination
|
23
22
|
builder.use middleware
|
@@ -25,4 +24,4 @@ module FlowChat
|
|
25
24
|
end
|
26
25
|
end
|
27
26
|
end
|
28
|
-
end
|
27
|
+
end
|
@@ -7,23 +7,31 @@ module FlowChat
|
|
7
7
|
@user_input = input
|
8
8
|
end
|
9
9
|
|
10
|
-
def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil)
|
10
|
+
def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil, media: nil)
|
11
11
|
if user_input.present?
|
12
12
|
input = user_input
|
13
13
|
input = convert.call(input) if convert.present?
|
14
14
|
validation_error = validate.call(input) if validate.present?
|
15
15
|
|
16
|
-
|
16
|
+
if validation_error.present?
|
17
|
+
# Include media URL in validation error message
|
18
|
+
original_message_with_media = build_message_with_media(msg, media)
|
19
|
+
prompt!([validation_error, original_message_with_media].join("\n\n"), choices:)
|
20
|
+
end
|
17
21
|
|
18
22
|
input = transform.call(input) if transform.present?
|
19
23
|
return input
|
20
24
|
end
|
21
25
|
|
22
|
-
|
26
|
+
# Include media URL in the message for USSD
|
27
|
+
final_message = build_message_with_media(msg, media)
|
28
|
+
prompt! final_message, choices:
|
23
29
|
end
|
24
30
|
|
25
|
-
def say(message)
|
26
|
-
|
31
|
+
def say(message, media: nil)
|
32
|
+
# Include media URL in the message for USSD
|
33
|
+
final_message = build_message_with_media(message, media)
|
34
|
+
terminate! final_message
|
27
35
|
end
|
28
36
|
|
29
37
|
def select(msg, choices)
|
@@ -43,6 +51,32 @@ module FlowChat
|
|
43
51
|
|
44
52
|
private
|
45
53
|
|
54
|
+
def build_message_with_media(message, media)
|
55
|
+
return message unless media
|
56
|
+
|
57
|
+
media_url = media[:url] || media[:path]
|
58
|
+
media_type = media[:type] || :image
|
59
|
+
|
60
|
+
# For USSD, we append the media URL to the message
|
61
|
+
media_text = case media_type.to_sym
|
62
|
+
when :image
|
63
|
+
"📷 Image: #{media_url}"
|
64
|
+
when :document
|
65
|
+
"📄 Document: #{media_url}"
|
66
|
+
when :audio
|
67
|
+
"🎵 Audio: #{media_url}"
|
68
|
+
when :video
|
69
|
+
"🎥 Video: #{media_url}"
|
70
|
+
when :sticker
|
71
|
+
"😊 Sticker: #{media_url}"
|
72
|
+
else
|
73
|
+
"📎 Media: #{media_url}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Combine message with media information
|
77
|
+
"#{message}\n\n#{media_text}"
|
78
|
+
end
|
79
|
+
|
46
80
|
def build_select_choices(choices)
|
47
81
|
case choices
|
48
82
|
when Array
|
data/lib/flow_chat/version.rb
CHANGED
@@ -17,7 +17,13 @@ module FlowChat
|
|
17
17
|
navigation_stack << key
|
18
18
|
return session.get(key) if session.get(key).present?
|
19
19
|
|
20
|
-
|
20
|
+
user_input = input
|
21
|
+
if session.get("$started_at$").nil?
|
22
|
+
session.set("$started_at$", Time.current.iso8601)
|
23
|
+
user_input = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
prompt = FlowChat::Whatsapp::Prompt.new user_input
|
21
27
|
@input = nil # input is being submitted to prompt so we clear it
|
22
28
|
|
23
29
|
value = yield prompt
|
@@ -55,4 +61,4 @@ module FlowChat
|
|
55
61
|
end
|
56
62
|
end
|
57
63
|
end
|
58
|
-
end
|
64
|
+
end
|
@@ -0,0 +1,435 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require "uri"
|
4
|
+
require "tempfile"
|
5
|
+
require "securerandom"
|
6
|
+
|
7
|
+
module FlowChat
|
8
|
+
module Whatsapp
|
9
|
+
class Client
|
10
|
+
WHATSAPP_API_URL = "https://graph.facebook.com/v18.0"
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
# Send a message to a WhatsApp number
|
17
|
+
# @param to [String] Phone number in E.164 format
|
18
|
+
# @param response [Array] FlowChat response array [type, content, options]
|
19
|
+
# @return [Hash] API response or nil on error
|
20
|
+
def send_message(to, response)
|
21
|
+
message_data = build_message_payload(response, to)
|
22
|
+
send_message_payload(message_data)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Send a text message
|
26
|
+
# @param to [String] Phone number in E.164 format
|
27
|
+
# @param text [String] Message text
|
28
|
+
# @return [Hash] API response or nil on error
|
29
|
+
def send_text(to, text)
|
30
|
+
send_message(to, [:text, text, {}])
|
31
|
+
end
|
32
|
+
|
33
|
+
# Send interactive buttons
|
34
|
+
# @param to [String] Phone number in E.164 format
|
35
|
+
# @param text [String] Message text
|
36
|
+
# @param buttons [Array] Array of button hashes with :id and :title
|
37
|
+
# @return [Hash] API response or nil on error
|
38
|
+
def send_buttons(to, text, buttons)
|
39
|
+
send_message(to, [:interactive_buttons, text, {buttons: buttons}])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Send interactive list
|
43
|
+
# @param to [String] Phone number in E.164 format
|
44
|
+
# @param text [String] Message text
|
45
|
+
# @param sections [Array] List sections
|
46
|
+
# @param button_text [String] Button text (default: "Choose")
|
47
|
+
# @return [Hash] API response or nil on error
|
48
|
+
def send_list(to, text, sections, button_text = "Choose")
|
49
|
+
send_message(to, [:interactive_list, text, {sections: sections, button_text: button_text}])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Send a template message
|
53
|
+
# @param to [String] Phone number in E.164 format
|
54
|
+
# @param template_name [String] Template name
|
55
|
+
# @param components [Array] Template components
|
56
|
+
# @param language [String] Language code (default: "en_US")
|
57
|
+
# @return [Hash] API response or nil on error
|
58
|
+
def send_template(to, template_name, components = [], language = "en_US")
|
59
|
+
send_message(to, [:template, "", {
|
60
|
+
template_name: template_name,
|
61
|
+
components: components,
|
62
|
+
language: language
|
63
|
+
}])
|
64
|
+
end
|
65
|
+
|
66
|
+
# Send image message
|
67
|
+
# @param to [String] Phone number in E.164 format
|
68
|
+
# @param image_url_or_id [String] Image URL or WhatsApp media ID
|
69
|
+
# @param caption [String] Optional caption
|
70
|
+
# @param mime_type [String] Optional MIME type for URLs (e.g., 'image/jpeg')
|
71
|
+
# @return [Hash] API response
|
72
|
+
def send_image(to, image_url_or_id, caption = nil, mime_type = nil)
|
73
|
+
send_media_message(to, :image, image_url_or_id, caption: caption, mime_type: mime_type)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Send document message
|
77
|
+
# @param to [String] Phone number in E.164 format
|
78
|
+
# @param document_url_or_id [String] Document URL or WhatsApp media ID
|
79
|
+
# @param caption [String] Optional caption
|
80
|
+
# @param filename [String] Optional filename
|
81
|
+
# @param mime_type [String] Optional MIME type for URLs (e.g., 'application/pdf')
|
82
|
+
# @return [Hash] API response
|
83
|
+
def send_document(to, document_url_or_id, caption = nil, filename = nil, mime_type = nil)
|
84
|
+
filename ||= extract_filename_from_url(document_url_or_id) if url?(document_url_or_id)
|
85
|
+
send_media_message(to, :document, document_url_or_id, caption: caption, filename: filename, mime_type: mime_type)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Send video message
|
89
|
+
# @param to [String] Phone number in E.164 format
|
90
|
+
# @param video_url_or_id [String] Video URL or WhatsApp media ID
|
91
|
+
# @param caption [String] Optional caption
|
92
|
+
# @param mime_type [String] Optional MIME type for URLs (e.g., 'video/mp4')
|
93
|
+
# @return [Hash] API response
|
94
|
+
def send_video(to, video_url_or_id, caption = nil, mime_type = nil)
|
95
|
+
send_media_message(to, :video, video_url_or_id, caption: caption, mime_type: mime_type)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Send audio message
|
99
|
+
# @param to [String] Phone number in E.164 format
|
100
|
+
# @param audio_url_or_id [String] Audio URL or WhatsApp media ID
|
101
|
+
# @param mime_type [String] Optional MIME type for URLs (e.g., 'audio/mpeg')
|
102
|
+
# @return [Hash] API response
|
103
|
+
def send_audio(to, audio_url_or_id, mime_type = nil)
|
104
|
+
send_media_message(to, :audio, audio_url_or_id, mime_type: mime_type)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Send sticker message
|
108
|
+
# @param to [String] Phone number in E.164 format
|
109
|
+
# @param sticker_url_or_id [String] Sticker URL or WhatsApp media ID
|
110
|
+
# @param mime_type [String] Optional MIME type for URLs (e.g., 'image/webp')
|
111
|
+
# @return [Hash] API response
|
112
|
+
def send_sticker(to, sticker_url_or_id, mime_type = nil)
|
113
|
+
send_media_message(to, :sticker, sticker_url_or_id, mime_type: mime_type)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Upload media file and return media ID
|
117
|
+
# @param file_path_or_io [String, IO] File path or IO object
|
118
|
+
# @param mime_type [String] MIME type of the file (required)
|
119
|
+
# @param filename [String] Optional filename for the upload
|
120
|
+
# @return [String] Media ID
|
121
|
+
# @raise [StandardError] If upload fails
|
122
|
+
def upload_media(file_path_or_io, mime_type, filename = nil)
|
123
|
+
raise ArgumentError, "mime_type is required" if mime_type.nil? || mime_type.empty?
|
124
|
+
|
125
|
+
if file_path_or_io.is_a?(String)
|
126
|
+
# File path
|
127
|
+
raise ArgumentError, "File not found: #{file_path_or_io}" unless File.exist?(file_path_or_io)
|
128
|
+
filename ||= File.basename(file_path_or_io)
|
129
|
+
file = File.open(file_path_or_io, "rb")
|
130
|
+
else
|
131
|
+
# IO object
|
132
|
+
file = file_path_or_io
|
133
|
+
filename ||= "upload"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Upload directly via HTTP
|
137
|
+
uri = URI("#{WHATSAPP_API_URL}/#{@config.phone_number_id}/media")
|
138
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
139
|
+
http.use_ssl = true
|
140
|
+
|
141
|
+
# Prepare multipart form data
|
142
|
+
boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
|
143
|
+
|
144
|
+
form_data = []
|
145
|
+
form_data << "--#{boundary}"
|
146
|
+
form_data << 'Content-Disposition: form-data; name="messaging_product"'
|
147
|
+
form_data << ""
|
148
|
+
form_data << "whatsapp"
|
149
|
+
|
150
|
+
form_data << "--#{boundary}"
|
151
|
+
form_data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\""
|
152
|
+
form_data << "Content-Type: #{mime_type}"
|
153
|
+
form_data << ""
|
154
|
+
form_data << file.read
|
155
|
+
|
156
|
+
form_data << "--#{boundary}"
|
157
|
+
form_data << 'Content-Disposition: form-data; name="type"'
|
158
|
+
form_data << ""
|
159
|
+
form_data << mime_type
|
160
|
+
|
161
|
+
form_data << "--#{boundary}--"
|
162
|
+
|
163
|
+
body = form_data.join("\r\n")
|
164
|
+
|
165
|
+
request = Net::HTTP::Post.new(uri)
|
166
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
167
|
+
request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
168
|
+
request.body = body
|
169
|
+
|
170
|
+
response = http.request(request)
|
171
|
+
|
172
|
+
if response.is_a?(Net::HTTPSuccess)
|
173
|
+
data = JSON.parse(response.body)
|
174
|
+
data["id"] || raise(StandardError, "Failed to upload media: #{data}")
|
175
|
+
else
|
176
|
+
Rails.logger.error "WhatsApp Media Upload error: #{response.body}"
|
177
|
+
raise StandardError, "Media upload failed: #{response.body}"
|
178
|
+
end
|
179
|
+
ensure
|
180
|
+
file&.close if file_path_or_io.is_a?(String)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Build message payload for WhatsApp API
|
184
|
+
# This method is exposed so the gateway can use it for simulator mode
|
185
|
+
def build_message_payload(response, to)
|
186
|
+
type, content, options = response
|
187
|
+
|
188
|
+
case type
|
189
|
+
when :text
|
190
|
+
{
|
191
|
+
messaging_product: "whatsapp",
|
192
|
+
to: to,
|
193
|
+
type: "text",
|
194
|
+
text: {body: content}
|
195
|
+
}
|
196
|
+
when :interactive_buttons
|
197
|
+
{
|
198
|
+
messaging_product: "whatsapp",
|
199
|
+
to: to,
|
200
|
+
type: "interactive",
|
201
|
+
interactive: {
|
202
|
+
type: "button",
|
203
|
+
body: {text: content},
|
204
|
+
action: {
|
205
|
+
buttons: options[:buttons].map.with_index do |button, index|
|
206
|
+
{
|
207
|
+
type: "reply",
|
208
|
+
reply: {
|
209
|
+
id: button[:id] || index.to_s,
|
210
|
+
title: button[:title]
|
211
|
+
}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
when :interactive_list
|
218
|
+
{
|
219
|
+
messaging_product: "whatsapp",
|
220
|
+
to: to,
|
221
|
+
type: "interactive",
|
222
|
+
interactive: {
|
223
|
+
type: "list",
|
224
|
+
body: {text: content},
|
225
|
+
action: {
|
226
|
+
button: options[:button_text] || "Choose",
|
227
|
+
sections: options[:sections]
|
228
|
+
}
|
229
|
+
}
|
230
|
+
}
|
231
|
+
when :template
|
232
|
+
{
|
233
|
+
messaging_product: "whatsapp",
|
234
|
+
to: to,
|
235
|
+
type: "template",
|
236
|
+
template: {
|
237
|
+
name: options[:template_name],
|
238
|
+
language: {code: options[:language] || "en_US"},
|
239
|
+
components: options[:components] || []
|
240
|
+
}
|
241
|
+
}
|
242
|
+
when :media_image
|
243
|
+
{
|
244
|
+
messaging_product: "whatsapp",
|
245
|
+
to: to,
|
246
|
+
type: "image",
|
247
|
+
image: build_media_object(options)
|
248
|
+
}
|
249
|
+
when :media_document
|
250
|
+
{
|
251
|
+
messaging_product: "whatsapp",
|
252
|
+
to: to,
|
253
|
+
type: "document",
|
254
|
+
document: build_media_object(options)
|
255
|
+
}
|
256
|
+
when :media_audio
|
257
|
+
{
|
258
|
+
messaging_product: "whatsapp",
|
259
|
+
to: to,
|
260
|
+
type: "audio",
|
261
|
+
audio: build_media_object(options)
|
262
|
+
}
|
263
|
+
when :media_video
|
264
|
+
{
|
265
|
+
messaging_product: "whatsapp",
|
266
|
+
to: to,
|
267
|
+
type: "video",
|
268
|
+
video: build_media_object(options)
|
269
|
+
}
|
270
|
+
when :media_sticker
|
271
|
+
{
|
272
|
+
messaging_product: "whatsapp",
|
273
|
+
to: to,
|
274
|
+
type: "sticker",
|
275
|
+
sticker: build_media_object(options)
|
276
|
+
}
|
277
|
+
else
|
278
|
+
# Default to text message
|
279
|
+
{
|
280
|
+
messaging_product: "whatsapp",
|
281
|
+
to: to,
|
282
|
+
type: "text",
|
283
|
+
text: {body: content.to_s}
|
284
|
+
}
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Get media URL from media ID
|
289
|
+
# @param media_id [String] Media ID from WhatsApp
|
290
|
+
# @return [String] Media URL or nil on error
|
291
|
+
def get_media_url(media_id)
|
292
|
+
uri = URI("#{WHATSAPP_API_URL}/#{media_id}")
|
293
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
294
|
+
http.use_ssl = true
|
295
|
+
|
296
|
+
request = Net::HTTP::Get.new(uri)
|
297
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
298
|
+
|
299
|
+
response = http.request(request)
|
300
|
+
|
301
|
+
if response.is_a?(Net::HTTPSuccess)
|
302
|
+
data = JSON.parse(response.body)
|
303
|
+
data["url"]
|
304
|
+
else
|
305
|
+
Rails.logger.error "WhatsApp Media API error: #{response.body}"
|
306
|
+
nil
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Download media content
|
311
|
+
# @param media_id [String] Media ID from WhatsApp
|
312
|
+
# @return [String] Media content or nil on error
|
313
|
+
def download_media(media_id)
|
314
|
+
media_url = get_media_url(media_id)
|
315
|
+
return nil unless media_url
|
316
|
+
|
317
|
+
uri = URI(media_url)
|
318
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
319
|
+
http.use_ssl = true
|
320
|
+
|
321
|
+
request = Net::HTTP::Get.new(uri)
|
322
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
323
|
+
|
324
|
+
response = http.request(request)
|
325
|
+
|
326
|
+
if response.is_a?(Net::HTTPSuccess)
|
327
|
+
response.body
|
328
|
+
else
|
329
|
+
Rails.logger.error "WhatsApp Media download error: #{response.body}"
|
330
|
+
nil
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Get MIME type from URL without downloading (HEAD request)
|
335
|
+
def get_media_mime_type(url)
|
336
|
+
require "net/http"
|
337
|
+
|
338
|
+
uri = URI(url)
|
339
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
340
|
+
http.use_ssl = (uri.scheme == "https")
|
341
|
+
|
342
|
+
# Use HEAD request to get headers without downloading content
|
343
|
+
response = http.head(uri.path)
|
344
|
+
response["content-type"]
|
345
|
+
rescue => e
|
346
|
+
Rails.logger.warn "Could not detect MIME type for #{url}: #{e.message}"
|
347
|
+
nil
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
# Build media object for WhatsApp API, handling both URLs and media IDs
|
353
|
+
# @param options [Hash] Options containing url/id, caption, filename
|
354
|
+
# @return [Hash] Media object for WhatsApp API
|
355
|
+
def build_media_object(options)
|
356
|
+
media_obj = {}
|
357
|
+
|
358
|
+
# Handle URL or ID
|
359
|
+
if options[:url]
|
360
|
+
# Use URL directly
|
361
|
+
media_obj[:link] = options[:url]
|
362
|
+
elsif options[:id]
|
363
|
+
# Use provided media ID directly
|
364
|
+
media_obj[:id] = options[:id]
|
365
|
+
end
|
366
|
+
|
367
|
+
# Add optional fields
|
368
|
+
media_obj[:caption] = options[:caption] if options[:caption]
|
369
|
+
media_obj[:filename] = options[:filename] if options[:filename]
|
370
|
+
|
371
|
+
media_obj
|
372
|
+
end
|
373
|
+
|
374
|
+
# Check if input is a URL or file path/media ID
|
375
|
+
def url?(input)
|
376
|
+
input.to_s.start_with?("http://", "https://")
|
377
|
+
end
|
378
|
+
|
379
|
+
# Extract filename from URL
|
380
|
+
def extract_filename_from_url(url)
|
381
|
+
uri = URI(url)
|
382
|
+
filename = File.basename(uri.path)
|
383
|
+
filename.empty? ? "document" : filename
|
384
|
+
rescue
|
385
|
+
"document"
|
386
|
+
end
|
387
|
+
|
388
|
+
# Send message payload to WhatsApp API
|
389
|
+
# @param message_data [Hash] Message payload
|
390
|
+
# @return [Hash] API response or nil on error
|
391
|
+
def send_message_payload(message_data)
|
392
|
+
uri = URI("#{WHATSAPP_API_URL}/#{@config.phone_number_id}/messages")
|
393
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
394
|
+
http.use_ssl = true
|
395
|
+
|
396
|
+
request = Net::HTTP::Post.new(uri)
|
397
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
398
|
+
request["Content-Type"] = "application/json"
|
399
|
+
request.body = message_data.to_json
|
400
|
+
|
401
|
+
response = http.request(request)
|
402
|
+
|
403
|
+
if response.is_a?(Net::HTTPSuccess)
|
404
|
+
JSON.parse(response.body)
|
405
|
+
else
|
406
|
+
Rails.logger.error "WhatsApp API error: #{response.body}"
|
407
|
+
nil
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def send_media_message(to, media_type, url_or_id, caption: nil, filename: nil, mime_type: nil)
|
412
|
+
media_object = if url?(url_or_id)
|
413
|
+
{link: url_or_id}
|
414
|
+
else
|
415
|
+
{id: url_or_id}
|
416
|
+
end
|
417
|
+
|
418
|
+
# Add caption if provided (stickers don't support captions)
|
419
|
+
media_object[:caption] = caption if caption && media_type != :sticker
|
420
|
+
|
421
|
+
# Add filename for documents
|
422
|
+
media_object[:filename] = filename if filename && media_type == :document
|
423
|
+
|
424
|
+
message = {
|
425
|
+
:messaging_product => "whatsapp",
|
426
|
+
:to => to,
|
427
|
+
:type => media_type.to_s,
|
428
|
+
media_type.to_s => media_object
|
429
|
+
}
|
430
|
+
|
431
|
+
send_message_payload(message)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
@@ -2,9 +2,13 @@ module FlowChat
|
|
2
2
|
module Whatsapp
|
3
3
|
class Configuration
|
4
4
|
attr_accessor :access_token, :phone_number_id, :verify_token, :app_id, :app_secret,
|
5
|
-
|
5
|
+
:webhook_url, :webhook_verify_token, :business_account_id, :name
|
6
6
|
|
7
|
-
|
7
|
+
# Class-level storage for named configurations
|
8
|
+
@@configurations = {}
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
8
12
|
@access_token = nil
|
9
13
|
@phone_number_id = nil
|
10
14
|
@verify_token = nil
|
@@ -13,12 +17,14 @@ module FlowChat
|
|
13
17
|
@webhook_url = nil
|
14
18
|
@webhook_verify_token = nil
|
15
19
|
@business_account_id = nil
|
20
|
+
|
21
|
+
register_as(name) if name.present?
|
16
22
|
end
|
17
23
|
|
18
24
|
# Load configuration from Rails credentials or environment variables
|
19
25
|
def self.from_credentials
|
20
|
-
config = new
|
21
|
-
|
26
|
+
config = new(nil)
|
27
|
+
|
22
28
|
if defined?(Rails) && Rails.application.credentials.whatsapp
|
23
29
|
credentials = Rails.application.credentials.whatsapp
|
24
30
|
config.access_token = credentials[:access_token]
|
@@ -30,18 +36,50 @@ module FlowChat
|
|
30
36
|
config.business_account_id = credentials[:business_account_id]
|
31
37
|
else
|
32
38
|
# Fallback to environment variables
|
33
|
-
config.access_token = ENV[
|
34
|
-
config.phone_number_id = ENV[
|
35
|
-
config.verify_token = ENV[
|
36
|
-
config.app_id = ENV[
|
37
|
-
config.app_secret = ENV[
|
38
|
-
config.webhook_url = ENV[
|
39
|
-
config.business_account_id = ENV[
|
39
|
+
config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
|
40
|
+
config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
|
41
|
+
config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
|
42
|
+
config.app_id = ENV["WHATSAPP_APP_ID"]
|
43
|
+
config.app_secret = ENV["WHATSAPP_APP_SECRET"]
|
44
|
+
config.webhook_url = ENV["WHATSAPP_WEBHOOK_URL"]
|
45
|
+
config.business_account_id = ENV["WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
40
46
|
end
|
41
47
|
|
42
48
|
config
|
43
49
|
end
|
44
50
|
|
51
|
+
# Register a named configuration
|
52
|
+
def self.register(name, config)
|
53
|
+
@@configurations[name.to_sym] = config
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get a named configuration
|
57
|
+
def self.get(name)
|
58
|
+
@@configurations[name.to_sym] || raise(ArgumentError, "WhatsApp configuration '#{name}' not found")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if a named configuration exists
|
62
|
+
def self.exists?(name)
|
63
|
+
@@configurations.key?(name.to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get all configuration names
|
67
|
+
def self.configuration_names
|
68
|
+
@@configurations.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
# Clear all registered configurations (useful for testing)
|
72
|
+
def self.clear_all!
|
73
|
+
@@configurations.clear
|
74
|
+
end
|
75
|
+
|
76
|
+
# Register this configuration with a name
|
77
|
+
def register_as(name)
|
78
|
+
@name = name.to_sym
|
79
|
+
self.class.register(@name, self)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
45
83
|
def valid?
|
46
84
|
access_token.present? && phone_number_id.present? && verify_token.present?
|
47
85
|
end
|
@@ -72,4 +110,4 @@ module FlowChat
|
|
72
110
|
end
|
73
111
|
end
|
74
112
|
end
|
75
|
-
end
|
113
|
+
end
|