flow_chat 0.4.0 → 0.4.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.
@@ -0,0 +1,406 @@
1
+ # WhatsApp Media Messaging Examples
2
+ # This file demonstrates how to send different types of media via WhatsApp using FlowChat
3
+
4
+ # ============================================================================
5
+ # BASIC MEDIA SENDING (Out-of-Band Messaging)
6
+ # ============================================================================
7
+
8
+ # Initialize the WhatsApp client
9
+ config = FlowChat::Whatsapp::Configuration.from_credentials
10
+ client = FlowChat::Whatsapp::Client.new(config)
11
+
12
+ # Send an image from URL with caption
13
+ client.send_image("+1234567890", "https://example.com/images/product.jpg", "Check out this amazing photo!")
14
+
15
+ # Send a document from URL
16
+ client.send_document("+1234567890", "https://example.com/reports/monthly_report.pdf", "Here's the monthly report")
17
+
18
+ # Send an audio file from URL
19
+ client.send_audio("+1234567890", "https://example.com/audio/greeting.mp3")
20
+
21
+ # Send a video from URL with caption
22
+ client.send_video("+1234567890", "https://example.com/videos/demo.mp4", "Product demo video")
23
+
24
+ # Send a sticker from URL
25
+ client.send_sticker("+1234567890", "https://example.com/stickers/happy.webp")
26
+
27
+ # You can also still use existing WhatsApp media IDs
28
+ client.send_image("+1234567890", "1234567890", "Image from existing media ID")
29
+
30
+ # ============================================================================
31
+ # USING MEDIA IN FLOWS
32
+ # ============================================================================
33
+
34
+ class MediaSupportFlow < FlowChat::Flow
35
+ def main_page
36
+ # Handle incoming media from user
37
+ if app.media
38
+ handle_received_media
39
+ return
40
+ end
41
+
42
+ choice = app.screen(:main_menu) do |prompt|
43
+ prompt.select "Welcome! How can I help you?", {
44
+ "catalog" => "📷 View Product Catalog",
45
+ "report" => "📄 Get Report",
46
+ "support" => "🎵 Voice Support",
47
+ "feedback" => "📝 Send Feedback"
48
+ }
49
+ end
50
+
51
+ case choice
52
+ when "catalog"
53
+ send_product_catalog
54
+ when "report"
55
+ send_report
56
+ when "support"
57
+ send_voice_message
58
+ when "feedback"
59
+ collect_feedback
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def handle_received_media
66
+ media_type = app.media['type']
67
+ media_id = app.media['id']
68
+
69
+ Rails.logger.info "Received #{media_type} from #{app.phone_number}: #{media_id}"
70
+
71
+ case media_type
72
+ when 'image'
73
+ app.say "Thanks for the image! I can see it's a #{media_type} file. Let me process it for you."
74
+ when 'document'
75
+ app.say "I've received your document. I'll review it and get back to you shortly."
76
+ when 'audio'
77
+ app.say "Got your voice message! I'll listen to it and respond appropriately."
78
+ when 'video'
79
+ app.say "Thanks for the video! I'll analyze it and provide feedback."
80
+ end
81
+ end
82
+
83
+ def send_product_catalog
84
+ # Send multiple product images from URLs
85
+ client = get_whatsapp_client
86
+
87
+ app.say "Here's our latest product catalog:"
88
+
89
+ # Product images stored in cloud storage (CDN, S3, etc.)
90
+ product_images = [
91
+ "https://cdn.example.com/products/product1.jpg",
92
+ "https://cdn.example.com/products/product2.jpg",
93
+ "https://cdn.example.com/products/product3.jpg"
94
+ ]
95
+
96
+ product_images.each_with_index do |image_url, index|
97
+ client.send_image(app.phone_number, image_url, "Product #{index + 1}")
98
+ sleep(0.5) # Small delay to avoid rate limiting
99
+ end
100
+
101
+ app.say "Which product interests you the most?"
102
+ end
103
+
104
+ def send_report
105
+ # Send a PDF report from cloud storage
106
+ report_url = generate_report_url # Your method to generate/get report URL
107
+
108
+ if report_url
109
+ client = get_whatsapp_client
110
+ client.send_document(app.phone_number, report_url, "Your monthly report is ready!")
111
+
112
+ app.say "📊 Report sent! Please check the document above."
113
+ else
114
+ app.say "Sorry, I couldn't generate the report right now. Please try again later."
115
+ end
116
+ end
117
+
118
+ def send_voice_message
119
+ # Send a pre-recorded voice message from cloud storage
120
+ audio_url = "https://cdn.example.com/audio/support_greeting.mp3"
121
+
122
+ client = get_whatsapp_client
123
+ client.send_audio(app.phone_number, audio_url)
124
+
125
+ app.say "🎵 Please listen to the voice message above. You can also send me a voice message with your question!"
126
+ end
127
+
128
+ def collect_feedback
129
+ feedback = app.screen(:feedback_text) do |prompt|
130
+ prompt.ask "Please share your feedback. You can also send images or documents if needed:"
131
+ end
132
+
133
+ # Save feedback to database
134
+ save_feedback(feedback, app.phone_number)
135
+
136
+ # Send a thank you sticker from cloud storage
137
+ sticker_url = "https://cdn.example.com/stickers/thanks.webp"
138
+ client = get_whatsapp_client
139
+ client.send_sticker(app.phone_number, sticker_url)
140
+
141
+ app.say "Thank you for your feedback! We really appreciate it. 🙏"
142
+ end
143
+
144
+ def get_whatsapp_client
145
+ config = FlowChat::Whatsapp::Configuration.from_credentials
146
+ FlowChat::Whatsapp::Client.new(config)
147
+ end
148
+
149
+ def generate_report_url
150
+ # Your report generation logic here
151
+ # This could return a signed URL from S3, Google Cloud Storage, etc.
152
+ "https://storage.example.com/reports/monthly_report_#{Time.current.strftime('%Y%m')}.pdf"
153
+ end
154
+
155
+ def save_feedback(feedback, phone_number)
156
+ # Your feedback saving logic here
157
+ Rails.logger.info "Feedback from #{phone_number}: #{feedback}"
158
+ end
159
+ end
160
+
161
+ # ============================================================================
162
+ # ADVANCED MEDIA SERVICE CLASS
163
+ # ============================================================================
164
+
165
+ class WhatsAppMediaService
166
+ def initialize
167
+ @config = FlowChat::Whatsapp::Configuration.from_credentials
168
+ @client = FlowChat::Whatsapp::Client.new(@config)
169
+ end
170
+
171
+ # Send welcome package with multiple media types from URLs
172
+ def send_welcome_package(phone_number, user_name)
173
+ # Welcome image from CDN
174
+ welcome_image_url = "https://cdn.example.com/welcome/banner.jpg"
175
+ @client.send_image(phone_number, welcome_image_url, "Welcome to our service, #{user_name}! 🎉")
176
+
177
+ # Welcome guide document from cloud storage
178
+ guide_url = "https://storage.example.com/guides/user_guide.pdf"
179
+ @client.send_document(phone_number, guide_url, "Here's your user guide")
180
+
181
+ # Welcome audio message from media server
182
+ audio_url = "https://media.example.com/audio/welcome.mp3"
183
+ @client.send_audio(phone_number, audio_url)
184
+ end
185
+
186
+ # Send order confirmation with invoice from cloud storage
187
+ def send_order_confirmation(phone_number, order_id, invoice_url)
188
+ # Send invoice document from cloud storage
189
+ @client.send_document(
190
+ phone_number,
191
+ invoice_url,
192
+ "Order ##{order_id} confirmed! Here's your invoice.",
193
+ "Invoice_#{order_id}.pdf"
194
+ )
195
+
196
+ # Send confirmation buttons
197
+ @client.send_buttons(
198
+ phone_number,
199
+ "Your order has been confirmed! 🛍️",
200
+ [
201
+ { id: 'track_order', title: '📦 Track Order' },
202
+ { id: 'modify_order', title: '✏️ Modify Order' },
203
+ { id: 'support', title: '💬 Contact Support' }
204
+ ]
205
+ )
206
+ end
207
+
208
+ # Send promotional content from CDN
209
+ def send_promotion(phone_number, promo_image_url, promo_video_url = nil)
210
+ # Send promotional image from CDN
211
+ @client.send_image(phone_number, promo_image_url, "🔥 Special offer just for you!")
212
+
213
+ # Optionally send promotional video from video server
214
+ if promo_video_url
215
+ @client.send_video(phone_number, promo_video_url, "Watch this exciting video!")
216
+ end
217
+
218
+ # Follow up with action buttons
219
+ @client.send_buttons(
220
+ phone_number,
221
+ "Don't miss out on this amazing deal!",
222
+ [
223
+ { id: 'buy_now', title: '🛒 Buy Now' },
224
+ { id: 'more_info', title: 'ℹ️ More Info' },
225
+ { id: 'remind_later', title: '⏰ Remind Later' }
226
+ ]
227
+ )
228
+ end
229
+
230
+ # Handle media uploads with processing
231
+ def process_uploaded_media(media_id, media_type, user_phone)
232
+ begin
233
+ # Download the media from WhatsApp
234
+ media_url = @client.get_media_url(media_id)
235
+ media_content = @client.download_media(media_id) if media_url
236
+
237
+ if media_content
238
+ # Upload to your cloud storage (S3, Google Cloud, etc.)
239
+ cloud_url = upload_to_cloud_storage(media_content, media_type, media_id)
240
+
241
+ # Process based on media type
242
+ case media_type
243
+ when 'image'
244
+ process_image(cloud_url, user_phone)
245
+ when 'document'
246
+ process_document(cloud_url, user_phone)
247
+ when 'audio'
248
+ process_audio(cloud_url, user_phone)
249
+ when 'video'
250
+ process_video(cloud_url, user_phone)
251
+ end
252
+
253
+ Rails.logger.info "Successfully processed #{media_type} from #{user_phone}"
254
+ end
255
+ rescue => e
256
+ Rails.logger.error "Error processing media: #{e.message}"
257
+ @client.send_text(user_phone, "Sorry, I couldn't process your file. Please try again.")
258
+ end
259
+ end
260
+
261
+ # Send personalized content based on user data
262
+ def send_personalized_content(phone_number, user_id)
263
+ # Get user's preferred content from your system
264
+ user_content = fetch_user_content(user_id)
265
+
266
+ # Send personalized image
267
+ if user_content[:image_url]
268
+ @client.send_image(phone_number, user_content[:image_url], user_content[:image_caption])
269
+ end
270
+
271
+ # Send personalized document
272
+ if user_content[:document_url]
273
+ @client.send_document(phone_number, user_content[:document_url], user_content[:document_description])
274
+ end
275
+ end
276
+
277
+ # Send real-time generated content
278
+ def send_qr_code(phone_number, data)
279
+ # Generate QR code and get URL (using your QR service)
280
+ qr_url = generate_qr_code_url(data)
281
+
282
+ @client.send_image(phone_number, qr_url, "Here's your QR code!")
283
+ end
284
+
285
+ # Send chart/graph from analytics service
286
+ def send_analytics_chart(phone_number, chart_type, period)
287
+ # Generate chart URL from your analytics service
288
+ chart_url = generate_analytics_chart_url(chart_type, period)
289
+
290
+ @client.send_image(phone_number, chart_url, "#{chart_type.humanize} for #{period}")
291
+ end
292
+
293
+ private
294
+
295
+ def upload_to_cloud_storage(content, media_type, media_id)
296
+ # Your cloud storage upload logic here
297
+ # Return the public URL of the uploaded file
298
+ "https://storage.example.com/uploads/#{media_id}.#{get_file_extension(media_type)}"
299
+ end
300
+
301
+ def process_image(cloud_url, user_phone)
302
+ # Your image processing logic here
303
+ @client.send_text(user_phone, "Thanks for the image! I've processed it successfully. ✅")
304
+ end
305
+
306
+ def process_document(cloud_url, user_phone)
307
+ # Your document processing logic here
308
+ @client.send_text(user_phone, "Document received and processed! 📄")
309
+ end
310
+
311
+ def process_audio(cloud_url, user_phone)
312
+ # Your audio processing logic here
313
+ @client.send_text(user_phone, "Audio message processed! 🎵")
314
+ end
315
+
316
+ def process_video(cloud_url, user_phone)
317
+ # Your video processing logic here
318
+ @client.send_text(user_phone, "Video processed successfully! 🎥")
319
+ end
320
+
321
+ def fetch_user_content(user_id)
322
+ # Fetch personalized content URLs from your database
323
+ {
324
+ image_url: "https://cdn.example.com/personal/#{user_id}/welcome.jpg",
325
+ image_caption: "Your personalized welcome image!",
326
+ document_url: "https://storage.example.com/personal/#{user_id}/guide.pdf",
327
+ document_description: "Your personalized guide"
328
+ }
329
+ end
330
+
331
+ def generate_qr_code_url(data)
332
+ # Generate QR code URL using your service (or external API like QR Server)
333
+ "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=#{CGI.escape(data)}"
334
+ end
335
+
336
+ def generate_analytics_chart_url(chart_type, period)
337
+ # Generate chart URL from your analytics service
338
+ "https://charts.example.com/api/generate?type=#{chart_type}&period=#{period}"
339
+ end
340
+
341
+ def get_file_extension(media_type)
342
+ case media_type
343
+ when 'image' then 'jpg'
344
+ when 'document' then 'pdf'
345
+ when 'audio' then 'mp3'
346
+ when 'video' then 'mp4'
347
+ else 'bin'
348
+ end
349
+ end
350
+ end
351
+
352
+ # ============================================================================
353
+ # USAGE IN CONTROLLERS
354
+ # ============================================================================
355
+
356
+ class NotificationController < ApplicationController
357
+ def send_media_notification
358
+ service = WhatsAppMediaService.new
359
+
360
+ # Send welcome package to new users
361
+ service.send_welcome_package(params[:phone_number], params[:user_name])
362
+
363
+ render json: { status: 'sent' }
364
+ end
365
+
366
+ def send_order_confirmation
367
+ service = WhatsAppMediaService.new
368
+
369
+ # Get invoice URL from your system (could be from S3, Google Cloud, etc.)
370
+ invoice_url = get_invoice_url(params[:order_id])
371
+
372
+ service.send_order_confirmation(
373
+ params[:phone_number],
374
+ params[:order_id],
375
+ invoice_url
376
+ )
377
+
378
+ render json: { status: 'sent' }
379
+ end
380
+
381
+ def send_promo
382
+ service = WhatsAppMediaService.new
383
+
384
+ # Promotional content from CDN
385
+ promo_image = "https://cdn.example.com/promos/#{params[:promo_id]}/banner.jpg"
386
+ promo_video = "https://cdn.example.com/promos/#{params[:promo_id]}/video.mp4"
387
+
388
+ service.send_promotion(params[:phone_number], promo_image, promo_video)
389
+
390
+ render json: { status: 'sent' }
391
+ end
392
+
393
+ def send_qr_code
394
+ service = WhatsAppMediaService.new
395
+ service.send_qr_code(params[:phone_number], params[:qr_data])
396
+
397
+ render json: { status: 'sent' }
398
+ end
399
+
400
+ private
401
+
402
+ def get_invoice_url(order_id)
403
+ # Your logic to get invoice URL from cloud storage
404
+ "https://storage.example.com/invoices/#{order_id}.pdf"
405
+ end
406
+ end
@@ -0,0 +1,111 @@
1
+ # Example Background Jobs for WhatsApp Response Delivery
2
+ # Add these to your Rails application
3
+
4
+ # Example: Basic WhatsApp Send Job
5
+ # Only handles sending responses - flows are processed synchronously in the controller
6
+ class WhatsappMessageJob < ApplicationJob
7
+ include FlowChat::Whatsapp::SendJobSupport
8
+
9
+ def perform(send_data)
10
+ perform_whatsapp_send(send_data)
11
+ end
12
+ end
13
+
14
+ # Example: Advanced WhatsApp Send Job with custom callbacks
15
+ class AdvancedWhatsappMessageJob < ApplicationJob
16
+ include FlowChat::Whatsapp::SendJobSupport
17
+
18
+ def perform(send_data)
19
+ perform_whatsapp_send(send_data)
20
+ end
21
+
22
+ private
23
+
24
+ # Override for custom success handling
25
+ def on_whatsapp_send_success(send_data, result)
26
+ Rails.logger.info "Successfully sent WhatsApp message to #{send_data[:msisdn]}"
27
+ UserEngagementTracker.track_message_sent(phone: send_data[:msisdn])
28
+ end
29
+
30
+ # Override for custom error handling
31
+ def on_whatsapp_send_error(error, send_data)
32
+ ErrorTracker.notify(error, user_phone: send_data[:msisdn])
33
+ end
34
+ end
35
+
36
+ # Example: Priority send job for urgent messages
37
+ class UrgentWhatsappSendJob < ApplicationJob
38
+ include FlowChat::Whatsapp::SendJobSupport
39
+
40
+ queue_as :urgent_whatsapp # Different queue for priority
41
+ retry_on StandardError, wait: 1.second, attempts: 5 # Override retry policy
42
+
43
+ def perform(send_data)
44
+ perform_whatsapp_send(send_data)
45
+ end
46
+
47
+ private
48
+
49
+ # Override error handling for urgent messages
50
+ def handle_whatsapp_send_error(error, send_data, config = nil)
51
+ # Immediately escalate urgent message failures
52
+ AlertingService.send_urgent_alert(
53
+ "Urgent WhatsApp send job failed",
54
+ error: error.message,
55
+ user: send_data[:msisdn]
56
+ )
57
+
58
+ # Still send user notification
59
+ super
60
+ end
61
+ end
62
+
63
+ # Example: Multi-tenant send job
64
+ class MultiTenantWhatsappSendJob < ApplicationJob
65
+ include FlowChat::Whatsapp::SendJobSupport
66
+
67
+ def perform(send_data)
68
+ perform_whatsapp_send(send_data)
69
+ end
70
+
71
+ private
72
+
73
+ # Override config resolution for tenant-specific configs
74
+ def resolve_whatsapp_config(send_data)
75
+ # Try tenant-specific config first
76
+ tenant_name = extract_tenant_from_phone(send_data[:msisdn])
77
+ if tenant_name && FlowChat::Whatsapp::Configuration.exists?(tenant_name)
78
+ return FlowChat::Whatsapp::Configuration.get(tenant_name)
79
+ end
80
+
81
+ # Fallback to default resolution
82
+ super
83
+ end
84
+
85
+ def extract_tenant_from_phone(phone)
86
+ # Extract tenant from phone number prefix or other identifier
87
+ case phone
88
+ when /^\+1800/
89
+ :enterprise
90
+ when /^\+1888/
91
+ :premium
92
+ else
93
+ :standard
94
+ end
95
+ end
96
+ end
97
+
98
+ # Usage in Rails configuration
99
+ #
100
+ # Add to config/application.rb:
101
+ # config.active_job.queue_adapter = :sidekiq
102
+ #
103
+ # Add to config/initializers/flowchat.rb:
104
+ # FlowChat::Config.whatsapp.message_handling_mode = :background
105
+ # FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
106
+ #
107
+ # How it works:
108
+ # 1. Controller receives WhatsApp webhook
109
+ # 2. Flow is processed synchronously (maintains controller context)
110
+ # 3. Response is queued for async delivery via background job
111
+ # 4. Job only handles sending the response, not processing flows
@@ -2,7 +2,7 @@ require "middleware"
2
2
 
3
3
  module FlowChat
4
4
  class BaseProcessor
5
- attr_reader :middleware, :gateway
5
+ attr_reader :middleware
6
6
 
7
7
  def initialize(controller)
8
8
  @context = FlowChat::Context.new
@@ -12,8 +12,9 @@ module FlowChat
12
12
  yield self if block_given?
13
13
  end
14
14
 
15
- def use_gateway(gateway)
16
- @gateway = gateway
15
+ def use_gateway(gateway_class, *args)
16
+ @gateway_class = gateway_class
17
+ @gateway_args = args
17
18
  self
18
19
  end
19
20
 
@@ -51,7 +52,10 @@ module FlowChat
51
52
 
52
53
  # Helper method for building stacks
53
54
  def create_middleware_stack(name)
55
+ raise ArgumentError, "Gateway is required. Call use_gateway(gateway_class, *args) before running." unless @gateway_class
56
+
54
57
  ::Middleware::Builder.new(name: name) do |b|
58
+ b.use @gateway_class, *@gateway_args
55
59
  configure_middleware_stack(b)
56
60
  end.inject_logger(Rails.logger)
57
61
  end
@@ -9,6 +9,11 @@ module FlowChat
9
9
  @ussd ||= UssdConfig.new
10
10
  end
11
11
 
12
+ # WhatsApp-specific configuration object
13
+ def self.whatsapp
14
+ @whatsapp ||= WhatsappConfig.new
15
+ end
16
+
12
17
  class UssdConfig
13
18
  attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
14
19
  :pagination_next_option, :pagination_next_text,
@@ -25,5 +30,36 @@ module FlowChat
25
30
  @resumable_sessions_timeout_seconds = 300
26
31
  end
27
32
  end
33
+
34
+ class WhatsappConfig
35
+ attr_accessor :message_handling_mode, :background_job_class
36
+
37
+ def initialize
38
+ @message_handling_mode = :inline
39
+ @background_job_class = 'WhatsappMessageJob'
40
+ end
41
+
42
+ # Validate message handling mode
43
+ def message_handling_mode=(mode)
44
+ valid_modes = [:inline, :background, :simulator]
45
+ unless valid_modes.include?(mode.to_sym)
46
+ raise ArgumentError, "Invalid message handling mode: #{mode}. Valid modes: #{valid_modes.join(', ')}"
47
+ end
48
+ @message_handling_mode = mode.to_sym
49
+ end
50
+
51
+ # Helper methods for mode checking
52
+ def inline_mode?
53
+ @message_handling_mode == :inline
54
+ end
55
+
56
+ def background_mode?
57
+ @message_handling_mode == :background
58
+ end
59
+
60
+ def simulator_mode?
61
+ @message_handling_mode == :simulator
62
+ end
63
+ end
28
64
  end
29
65
  end
@@ -0,0 +1,78 @@
1
+ module FlowChat
2
+ module Simulator
3
+ module Controller
4
+ def flowchat_simulator
5
+ respond_to do |format|
6
+ format.html do
7
+ render inline: simulator_view_template, layout: false, locals: simulator_locals
8
+ end
9
+ end
10
+ end
11
+
12
+ protected
13
+
14
+ def default_phone_number
15
+ "+233244123456"
16
+ end
17
+
18
+ def default_contact_name
19
+ "John Doe"
20
+ end
21
+
22
+ def default_config_key
23
+ "ussd"
24
+ end
25
+
26
+ def simulator_configurations
27
+ {
28
+ "ussd" => {
29
+ name: "USSD (Nalo)",
30
+ description: "Local development USSD testing",
31
+ processor_type: "ussd",
32
+ provider: "nalo",
33
+ endpoint: "/ussd",
34
+ icon: "📱",
35
+ color: "#28a745",
36
+ settings: {
37
+ phone_number: default_phone_number,
38
+ session_timeout: 300
39
+ }
40
+ },
41
+ "whatsapp" => {
42
+ name: "WhatsApp",
43
+ description: "Local development WhatsApp testing",
44
+ processor_type: "whatsapp",
45
+ provider: "cloud_api",
46
+ endpoint: "/whatsapp/webhook",
47
+ icon: "💬",
48
+ color: "#25D366",
49
+ settings: {
50
+ phone_number: default_phone_number,
51
+ contact_name: default_contact_name,
52
+ verify_token: "local_verify_token",
53
+ webhook_url: "http://localhost:3000/whatsapp/webhook"
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ def simulator_view_template
60
+ File.read simulator_view_path
61
+ end
62
+
63
+ def simulator_view_path
64
+ File.join FlowChat.root.join("flow_chat", "simulator", "views", "simulator.html.erb")
65
+ end
66
+
67
+ def simulator_locals
68
+ {
69
+ pagesize: FlowChat::Config.ussd.pagination_page_size,
70
+ default_phone_number: default_phone_number,
71
+ default_contact_name: default_contact_name,
72
+ default_config_key: default_config_key,
73
+ configurations: simulator_configurations
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end