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
@@ -11,6 +11,7 @@ module FlowChat
|
|
11
11
|
def initialize(app, config = nil)
|
12
12
|
@app = app
|
13
13
|
@config = config || FlowChat::Whatsapp::Configuration.from_credentials
|
14
|
+
@client = FlowChat::Whatsapp::Client.new(@config)
|
14
15
|
end
|
15
16
|
|
16
17
|
def call(context)
|
@@ -30,14 +31,27 @@ module FlowChat
|
|
30
31
|
controller.head :bad_request
|
31
32
|
end
|
32
33
|
|
34
|
+
# Expose client for out-of-band messaging
|
35
|
+
attr_reader :client
|
36
|
+
|
33
37
|
private
|
34
38
|
|
39
|
+
def determine_message_handler(context)
|
40
|
+
# Check for simulator parameter in request (highest priority)
|
41
|
+
if context["simulator_mode"] || context.controller.request.params["simulator_mode"]
|
42
|
+
return :simulator
|
43
|
+
end
|
44
|
+
|
45
|
+
# Use global WhatsApp configuration
|
46
|
+
FlowChat::Config.whatsapp.message_handling_mode
|
47
|
+
end
|
48
|
+
|
35
49
|
def handle_verification(context)
|
36
50
|
controller = context.controller
|
37
51
|
params = controller.request.params
|
38
52
|
|
39
53
|
verify_token = @config.verify_token
|
40
|
-
|
54
|
+
|
41
55
|
if params["hub.verify_token"] == verify_token
|
42
56
|
controller.render plain: params["hub.challenge"]
|
43
57
|
else
|
@@ -49,6 +63,11 @@ module FlowChat
|
|
49
63
|
controller = context.controller
|
50
64
|
body = JSON.parse(controller.request.body.read)
|
51
65
|
|
66
|
+
# Check for simulator mode parameter in request
|
67
|
+
if body.dig("simulator_mode") || controller.request.params["simulator_mode"]
|
68
|
+
context["simulator_mode"] = true
|
69
|
+
end
|
70
|
+
|
52
71
|
# Extract message data from WhatsApp webhook
|
53
72
|
entry = body.dig("entry", 0)
|
54
73
|
return controller.head :ok unless entry
|
@@ -72,142 +91,121 @@ module FlowChat
|
|
72
91
|
context["request.timestamp"] = message["timestamp"]
|
73
92
|
|
74
93
|
# Extract message content based on type
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
name: message.dig("location", "name"),
|
90
|
-
address: message.dig("location", "address")
|
91
|
-
}
|
92
|
-
# Set input so screen can proceed
|
93
|
-
context.input = "$location$"
|
94
|
-
when "image", "document", "audio", "video"
|
95
|
-
context["request.media"] = {
|
96
|
-
type: message["type"],
|
97
|
-
id: message.dig(message["type"], "id"),
|
98
|
-
mime_type: message.dig(message["type"], "mime_type"),
|
99
|
-
caption: message.dig(message["type"], "caption")
|
100
|
-
}
|
101
|
-
# Set input so screen can proceed
|
102
|
-
context.input = "$media$"
|
94
|
+
extract_message_content(message, context)
|
95
|
+
|
96
|
+
# Determine message handling mode
|
97
|
+
handler_mode = determine_message_handler(context)
|
98
|
+
|
99
|
+
# Process the message based on handling mode
|
100
|
+
case handler_mode
|
101
|
+
when :inline
|
102
|
+
handle_message_inline(context, controller)
|
103
|
+
when :background
|
104
|
+
handle_message_background(context, controller)
|
105
|
+
when :simulator
|
106
|
+
# Return early from simulator mode to preserve the JSON response
|
107
|
+
return handle_message_simulator(context, controller)
|
103
108
|
end
|
104
|
-
|
105
|
-
response = @app.call(context)
|
106
|
-
send_whatsapp_message(context, response)
|
107
109
|
end
|
108
110
|
|
109
111
|
# Handle message status updates
|
110
112
|
if value["statuses"]&.any?
|
111
|
-
# Log status updates but don't process them
|
112
113
|
Rails.logger.info "WhatsApp status update: #{value["statuses"]}"
|
113
114
|
end
|
114
115
|
|
115
116
|
controller.head :ok
|
116
117
|
end
|
117
118
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
119
|
+
def extract_message_content(message, context)
|
120
|
+
case message["type"]
|
121
|
+
when "text"
|
122
|
+
context.input = message.dig("text", "body")
|
123
|
+
when "interactive"
|
124
|
+
# Handle button/list replies
|
125
|
+
if message.dig("interactive", "type") == "button_reply"
|
126
|
+
context.input = message.dig("interactive", "button_reply", "id")
|
127
|
+
elsif message.dig("interactive", "type") == "list_reply"
|
128
|
+
context.input = message.dig("interactive", "list_reply", "id")
|
129
|
+
end
|
130
|
+
when "location"
|
131
|
+
context["request.location"] = {
|
132
|
+
latitude: message.dig("location", "latitude"),
|
133
|
+
longitude: message.dig("location", "longitude"),
|
134
|
+
name: message.dig("location", "name"),
|
135
|
+
address: message.dig("location", "address")
|
136
|
+
}
|
137
|
+
context.input = "$location$"
|
138
|
+
when "image", "document", "audio", "video"
|
139
|
+
context["request.media"] = {
|
140
|
+
type: message["type"],
|
141
|
+
id: message.dig(message["type"], "id"),
|
142
|
+
mime_type: message.dig(message["type"], "mime_type"),
|
143
|
+
caption: message.dig(message["type"], "caption")
|
144
|
+
}
|
145
|
+
context.input = "$media$"
|
146
|
+
end
|
147
|
+
end
|
124
148
|
|
125
|
-
|
149
|
+
def handle_message_inline(context, controller)
|
150
|
+
response = @app.call(context)
|
151
|
+
if response
|
152
|
+
result = @client.send_message(context["request.msisdn"], response)
|
153
|
+
context["whatsapp.message_result"] = result
|
154
|
+
end
|
155
|
+
end
|
126
156
|
|
127
|
-
|
128
|
-
|
129
|
-
|
157
|
+
def handle_message_background(context, controller)
|
158
|
+
# Process the flow synchronously (maintaining controller context)
|
159
|
+
response = @app.call(context)
|
130
160
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
161
|
+
if response
|
162
|
+
# Queue only the response delivery asynchronously
|
163
|
+
send_data = {
|
164
|
+
msisdn: context["request.msisdn"],
|
165
|
+
response: response,
|
166
|
+
config_name: @config.name
|
167
|
+
}
|
135
168
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
169
|
+
# Get job class from configuration
|
170
|
+
job_class_name = FlowChat::Config.whatsapp.background_job_class
|
171
|
+
|
172
|
+
# Enqueue background job for sending only
|
173
|
+
begin
|
174
|
+
job_class = job_class_name.constantize
|
175
|
+
job_class.perform_later(send_data)
|
176
|
+
rescue NameError
|
177
|
+
# Fallback to inline sending if no job system
|
178
|
+
Rails.logger.warn "Background mode requested but no #{job_class_name} found. Falling back to inline sending."
|
179
|
+
result = @client.send_message(context["request.msisdn"], response)
|
180
|
+
context["whatsapp.message_result"] = result
|
181
|
+
end
|
140
182
|
end
|
141
183
|
end
|
142
184
|
|
143
|
-
def
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
interactive: {
|
160
|
-
type: "button",
|
161
|
-
body: { text: content },
|
162
|
-
action: {
|
163
|
-
buttons: options[:buttons].map.with_index do |button, index|
|
164
|
-
{
|
165
|
-
type: "reply",
|
166
|
-
reply: {
|
167
|
-
id: button[:id] || index.to_s,
|
168
|
-
title: button[:title]
|
169
|
-
}
|
170
|
-
}
|
171
|
-
end
|
172
|
-
}
|
185
|
+
def handle_message_simulator(context, controller)
|
186
|
+
response = @app.call(context)
|
187
|
+
|
188
|
+
if response
|
189
|
+
# For simulator mode, return the response data in the HTTP response
|
190
|
+
# instead of actually sending via WhatsApp API
|
191
|
+
message_payload = @client.build_message_payload(response, context["request.msisdn"])
|
192
|
+
|
193
|
+
simulator_response = {
|
194
|
+
mode: "simulator",
|
195
|
+
webhook_processed: true,
|
196
|
+
would_send: message_payload,
|
197
|
+
message_info: {
|
198
|
+
to: context["request.msisdn"],
|
199
|
+
contact_name: context["request.contact_name"],
|
200
|
+
timestamp: Time.now.iso8601
|
173
201
|
}
|
174
202
|
}
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
to: to,
|
179
|
-
type: "interactive",
|
180
|
-
interactive: {
|
181
|
-
type: "list",
|
182
|
-
body: { text: content },
|
183
|
-
action: {
|
184
|
-
button: options[:button_text] || "Choose",
|
185
|
-
sections: options[:sections]
|
186
|
-
}
|
187
|
-
}
|
188
|
-
}
|
189
|
-
when :template
|
190
|
-
{
|
191
|
-
messaging_product: "whatsapp",
|
192
|
-
to: to,
|
193
|
-
type: "template",
|
194
|
-
template: {
|
195
|
-
name: options[:template_name],
|
196
|
-
language: { code: options[:language] || "en_US" },
|
197
|
-
components: options[:components] || []
|
198
|
-
}
|
199
|
-
}
|
200
|
-
else
|
201
|
-
# Default to text message
|
202
|
-
{
|
203
|
-
messaging_product: "whatsapp",
|
204
|
-
to: to,
|
205
|
-
type: "text",
|
206
|
-
text: { body: content.to_s }
|
207
|
-
}
|
203
|
+
|
204
|
+
controller.render json: simulator_response
|
205
|
+
nil
|
208
206
|
end
|
209
207
|
end
|
210
208
|
end
|
211
209
|
end
|
212
210
|
end
|
213
|
-
end
|
211
|
+
end
|
@@ -6,15 +6,6 @@ module FlowChat
|
|
6
6
|
self
|
7
7
|
end
|
8
8
|
|
9
|
-
def use_gateway(gateway_class)
|
10
|
-
if gateway_class == FlowChat::Whatsapp::Gateway::CloudApi
|
11
|
-
@gateway = @whatsapp_config ? gateway_class.new(nil, @whatsapp_config) : gateway_class.new(nil)
|
12
|
-
else
|
13
|
-
@gateway = gateway_class.new(nil)
|
14
|
-
end
|
15
|
-
self
|
16
|
-
end
|
17
|
-
|
18
9
|
protected
|
19
10
|
|
20
11
|
def middleware_name
|
@@ -26,11 +17,10 @@ module FlowChat
|
|
26
17
|
end
|
27
18
|
|
28
19
|
def configure_middleware_stack(builder)
|
29
|
-
builder.use gateway
|
30
20
|
builder.use FlowChat::Session::Middleware
|
31
21
|
builder.use middleware
|
32
22
|
builder.use FlowChat::Whatsapp::Middleware::Executor
|
33
23
|
end
|
34
24
|
end
|
35
25
|
end
|
36
|
-
end
|
26
|
+
end
|
@@ -7,14 +7,33 @@ module FlowChat
|
|
7
7
|
@input = input
|
8
8
|
end
|
9
9
|
|
10
|
-
def ask(message, transform: nil, validate: nil, convert: nil)
|
10
|
+
def ask(message, transform: nil, validate: nil, convert: nil, media: nil)
|
11
11
|
if input.present?
|
12
|
-
|
13
|
-
|
12
|
+
begin
|
13
|
+
processed_input = process_input(input, transform, validate, convert)
|
14
|
+
return processed_input unless processed_input.nil?
|
15
|
+
rescue FlowChat::Interrupt::Prompt => validation_error
|
16
|
+
# If validation failed, include the error message with the original prompt
|
17
|
+
error_message = validation_error.prompt[1]
|
18
|
+
combined_message = "#{message}\n\n#{error_message}"
|
19
|
+
raise FlowChat::Interrupt::Prompt.new([:text, combined_message, {}])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Send message and wait for response, optionally with media
|
24
|
+
if media
|
25
|
+
raise FlowChat::Interrupt::Prompt.new(build_media_prompt(message, media))
|
26
|
+
else
|
27
|
+
raise FlowChat::Interrupt::Prompt.new([:text, message, {}])
|
14
28
|
end
|
29
|
+
end
|
15
30
|
|
16
|
-
|
17
|
-
|
31
|
+
def say(message, media: nil)
|
32
|
+
if media
|
33
|
+
raise FlowChat::Interrupt::Terminate.new(build_media_prompt(message, media))
|
34
|
+
else
|
35
|
+
raise FlowChat::Interrupt::Terminate.new([:text, message, {}])
|
36
|
+
end
|
18
37
|
end
|
19
38
|
|
20
39
|
def select(message, choices, transform: nil, validate: nil, convert: nil)
|
@@ -26,39 +45,117 @@ module FlowChat
|
|
26
45
|
# Validate choices
|
27
46
|
validate_choices(choices)
|
28
47
|
|
48
|
+
# Standard selection without media support
|
49
|
+
interactive_prompt = build_selection_prompt(message, choices)
|
50
|
+
raise FlowChat::Interrupt::Prompt.new(interactive_prompt)
|
51
|
+
end
|
52
|
+
|
53
|
+
def yes?(message, transform: nil, validate: nil, convert: nil)
|
54
|
+
if input.present?
|
55
|
+
processed_input = process_boolean(input, transform, validate, convert)
|
56
|
+
return processed_input unless processed_input.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
buttons = [
|
60
|
+
{id: "yes", title: "Yes"},
|
61
|
+
{id: "no", title: "No"}
|
62
|
+
]
|
63
|
+
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, {buttons: buttons}])
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def build_media_prompt(message, media)
|
69
|
+
media_type = media[:type] || :image
|
70
|
+
url = media[:url] || media[:path]
|
71
|
+
filename = media[:filename]
|
72
|
+
|
73
|
+
case media_type.to_sym
|
74
|
+
when :image
|
75
|
+
[:media_image, "", {url: url, caption: message}]
|
76
|
+
when :document
|
77
|
+
[:media_document, "", {url: url, caption: message, filename: filename}]
|
78
|
+
when :audio
|
79
|
+
[:media_audio, "", {url: url, caption: message}]
|
80
|
+
when :video
|
81
|
+
[:media_video, "", {url: url, caption: message}]
|
82
|
+
when :sticker
|
83
|
+
[:media_sticker, "", {url: url}] # Stickers don't support captions
|
84
|
+
else
|
85
|
+
raise ArgumentError, "Unsupported media type: #{media_type}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_selection_prompt(message, choices)
|
29
90
|
# Determine the best way to present choices
|
30
91
|
if choices.is_a?(Array)
|
31
92
|
# Convert array to hash with index-based keys
|
32
93
|
choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
|
33
|
-
|
94
|
+
build_list_prompt(message, choice_hash)
|
34
95
|
elsif choices.is_a?(Hash)
|
35
96
|
if choices.length <= 3
|
36
97
|
# Use buttons for 3 or fewer choices
|
37
|
-
|
98
|
+
build_buttons_prompt(message, choices)
|
38
99
|
else
|
39
100
|
# Use list for more than 3 choices
|
40
|
-
|
101
|
+
build_list_prompt(message, choices)
|
41
102
|
end
|
42
103
|
else
|
43
104
|
raise ArgumentError, "choices must be an Array or Hash"
|
44
105
|
end
|
45
106
|
end
|
46
107
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
108
|
+
def build_buttons_prompt(message, choices)
|
109
|
+
buttons = choices.map do |key, value|
|
110
|
+
{
|
111
|
+
id: key.to_s,
|
112
|
+
title: truncate_text(value.to_s, 20) # WhatsApp button titles have a 20 character limit
|
113
|
+
}
|
51
114
|
end
|
52
115
|
|
53
|
-
buttons
|
54
|
-
{ id: "yes", title: "Yes" },
|
55
|
-
{ id: "no", title: "No" }
|
56
|
-
]
|
57
|
-
|
58
|
-
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, { buttons: buttons }])
|
116
|
+
[:interactive_buttons, message, {buttons: buttons}]
|
59
117
|
end
|
60
118
|
|
61
|
-
|
119
|
+
def build_list_prompt(message, choices)
|
120
|
+
items = choices.map do |key, value|
|
121
|
+
original_text = value.to_s
|
122
|
+
truncated_title = truncate_text(original_text, 24)
|
123
|
+
|
124
|
+
# If title was truncated, put full text in description (up to 72 chars)
|
125
|
+
description = if original_text.length > 24
|
126
|
+
truncate_text(original_text, 72)
|
127
|
+
end
|
128
|
+
|
129
|
+
{
|
130
|
+
id: key.to_s,
|
131
|
+
title: truncated_title,
|
132
|
+
description: description
|
133
|
+
}.compact
|
134
|
+
end
|
135
|
+
|
136
|
+
# If 10 or fewer items, use single section
|
137
|
+
sections = if items.length <= 10
|
138
|
+
[
|
139
|
+
{
|
140
|
+
title: "Options",
|
141
|
+
rows: items
|
142
|
+
}
|
143
|
+
]
|
144
|
+
else
|
145
|
+
# Paginate into multiple sections (max 10 items per section)
|
146
|
+
items.each_slice(10).with_index.map do |section_items, index|
|
147
|
+
start_num = (index * 10) + 1
|
148
|
+
end_num = start_num + section_items.length - 1
|
149
|
+
|
150
|
+
{
|
151
|
+
title: "#{start_num}-#{end_num}",
|
152
|
+
rows: section_items
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
[:interactive_list, message, {sections: sections}]
|
158
|
+
end
|
62
159
|
|
63
160
|
def process_input(input, transform, validate, convert)
|
64
161
|
# Apply transformation
|
@@ -79,8 +176,8 @@ module FlowChat
|
|
79
176
|
end
|
80
177
|
|
81
178
|
def process_selection(input, choices, transform, validate, convert)
|
82
|
-
choice_hash = choices.is_a?(Array) ?
|
83
|
-
choices.each_with_index.to_h { |choice, index| [index.to_s, choice] } :
|
179
|
+
choice_hash = choices.is_a?(Array) ?
|
180
|
+
choices.each_with_index.to_h { |choice, index| [index.to_s, choice] } :
|
84
181
|
choices
|
85
182
|
|
86
183
|
# Check if input matches a valid choice
|
@@ -100,13 +197,11 @@ module FlowChat
|
|
100
197
|
|
101
198
|
def process_boolean(input, transform, validate, convert)
|
102
199
|
boolean_value = case input.to_s.downcase
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
nil
|
109
|
-
end
|
200
|
+
when "yes", "y", "1", "true"
|
201
|
+
true
|
202
|
+
when "no", "n", "0", "false"
|
203
|
+
false
|
204
|
+
end
|
110
205
|
|
111
206
|
if boolean_value.nil?
|
112
207
|
raise FlowChat::Interrupt::Prompt.new([:text, "Please answer with Yes or No.", {}])
|
@@ -115,67 +210,13 @@ module FlowChat
|
|
115
210
|
process_input(boolean_value, transform, validate, convert)
|
116
211
|
end
|
117
212
|
|
118
|
-
def present_choices_as_buttons(message, choices)
|
119
|
-
buttons = choices.map do |key, value|
|
120
|
-
{
|
121
|
-
id: key.to_s,
|
122
|
-
title: truncate_text(value.to_s, 20) # WhatsApp button titles have a 20 character limit
|
123
|
-
}
|
124
|
-
end
|
125
|
-
|
126
|
-
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, { buttons: buttons }])
|
127
|
-
end
|
128
|
-
|
129
|
-
def present_choices_as_list(message, choices)
|
130
|
-
items = choices.map do |key, value|
|
131
|
-
original_text = value.to_s
|
132
|
-
truncated_title = truncate_text(original_text, 24)
|
133
|
-
|
134
|
-
# If title was truncated, put full text in description (up to 72 chars)
|
135
|
-
description = if original_text.length > 24
|
136
|
-
truncate_text(original_text, 72)
|
137
|
-
else
|
138
|
-
nil
|
139
|
-
end
|
140
|
-
|
141
|
-
{
|
142
|
-
id: key.to_s,
|
143
|
-
title: truncated_title,
|
144
|
-
description: description
|
145
|
-
}.compact
|
146
|
-
end
|
147
|
-
|
148
|
-
# If 10 or fewer items, use single section
|
149
|
-
if items.length <= 10
|
150
|
-
sections = [
|
151
|
-
{
|
152
|
-
title: "Options",
|
153
|
-
rows: items
|
154
|
-
}
|
155
|
-
]
|
156
|
-
else
|
157
|
-
# Paginate into multiple sections (max 10 items per section)
|
158
|
-
sections = items.each_slice(10).with_index.map do |section_items, index|
|
159
|
-
start_num = (index * 10) + 1
|
160
|
-
end_num = start_num + section_items.length - 1
|
161
|
-
|
162
|
-
{
|
163
|
-
title: "#{start_num}-#{end_num}",
|
164
|
-
rows: section_items
|
165
|
-
}
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
raise FlowChat::Interrupt::Prompt.new([:interactive_list, message, { sections: sections }])
|
170
|
-
end
|
171
|
-
|
172
213
|
def validate_choices(choices)
|
173
214
|
# Check for empty choices
|
174
215
|
if choices.nil? || choices.empty?
|
175
216
|
raise ArgumentError, "choices cannot be empty"
|
176
217
|
end
|
177
218
|
|
178
|
-
choice_count = choices.
|
219
|
+
choice_count = choices.length
|
179
220
|
|
180
221
|
# WhatsApp supports max 100 total items across all sections
|
181
222
|
if choice_count > 100
|
@@ -203,4 +244,4 @@ module FlowChat
|
|
203
244
|
end
|
204
245
|
end
|
205
246
|
end
|
206
|
-
end
|
247
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Whatsapp
|
3
|
+
# Module to be included in background jobs for WhatsApp response delivery
|
4
|
+
# Only handles sending responses, not processing flows
|
5
|
+
module SendJobSupport
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# Set up job configuration
|
10
|
+
queue_as :default
|
11
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
12
|
+
end
|
13
|
+
|
14
|
+
# Main job execution method for sending responses
|
15
|
+
def perform_whatsapp_send(send_data)
|
16
|
+
config = resolve_whatsapp_config(send_data)
|
17
|
+
client = FlowChat::Whatsapp::Client.new(config)
|
18
|
+
|
19
|
+
result = client.send_message(send_data[:msisdn], send_data[:response])
|
20
|
+
|
21
|
+
if result
|
22
|
+
Rails.logger.info "WhatsApp message sent successfully: #{result["messages"]&.first&.dig("id")}"
|
23
|
+
on_whatsapp_send_success(send_data, result)
|
24
|
+
else
|
25
|
+
Rails.logger.error "Failed to send WhatsApp message to #{send_data[:msisdn]}"
|
26
|
+
raise "WhatsApp API call failed"
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
on_whatsapp_send_error(e, send_data)
|
30
|
+
handle_whatsapp_send_error(e, send_data, config)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Resolve WhatsApp configuration by name or fallback
|
36
|
+
def resolve_whatsapp_config(send_data)
|
37
|
+
# Try to resolve by name first (preferred method)
|
38
|
+
if send_data[:config_name] && FlowChat::Whatsapp::Configuration.exists?(send_data[:config_name])
|
39
|
+
return FlowChat::Whatsapp::Configuration.get(send_data[:config_name])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Final fallback to default configuration
|
43
|
+
FlowChat::Whatsapp::Configuration.from_credentials
|
44
|
+
end
|
45
|
+
|
46
|
+
# Handle errors with user notification
|
47
|
+
def handle_whatsapp_send_error(error, send_data, config = nil)
|
48
|
+
Rails.logger.error "WhatsApp send job error: #{error.message}"
|
49
|
+
Rails.logger.error error.backtrace&.join("\n") if error.backtrace
|
50
|
+
|
51
|
+
# Try to send error message to user if we have config
|
52
|
+
if config
|
53
|
+
begin
|
54
|
+
client = FlowChat::Whatsapp::Client.new(config)
|
55
|
+
client.send_text(
|
56
|
+
send_data[:msisdn],
|
57
|
+
"⚠️ We're experiencing technical difficulties. Please try again in a few minutes."
|
58
|
+
)
|
59
|
+
rescue => send_error
|
60
|
+
Rails.logger.error "Failed to send error message: #{send_error.message}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Re-raise for job retry logic
|
65
|
+
raise error
|
66
|
+
end
|
67
|
+
|
68
|
+
# Override this method in your job for custom behavior
|
69
|
+
def on_whatsapp_send_success(send_data, result)
|
70
|
+
# Optional callback for successful message sending
|
71
|
+
end
|
72
|
+
|
73
|
+
# Override this method in your job for custom error handling
|
74
|
+
def on_whatsapp_send_error(error, send_data)
|
75
|
+
# Optional callback for error handling
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|