flow_chat 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +50 -5
- data/app/controllers/demo_controller.rb +101 -0
- data/app/flow_chat/demo_restaurant_flow.rb +889 -0
- data/config/routes_demo.rb +59 -0
- data/lib/flow_chat/config.rb +3 -0
- data/lib/flow_chat/interrupt.rb +6 -5
- data/lib/flow_chat/prompt.rb +91 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +0 -2
- data/lib/flow_chat/ussd/app.rb +2 -2
- data/lib/flow_chat/ussd/gateway/nalo.rb +4 -4
- data/lib/flow_chat/ussd/middleware/executor.rb +2 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +35 -18
- data/lib/flow_chat/ussd/renderer.rb +28 -3
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +3 -3
- data/lib/flow_chat/whatsapp/client.rb +22 -15
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +36 -12
- data/lib/flow_chat/whatsapp/middleware/executor.rb +3 -3
- data/lib/flow_chat/whatsapp/renderer.rb +191 -0
- metadata +6 -3
- data/lib/flow_chat/ussd/prompt.rb +0 -102
- data/lib/flow_chat/whatsapp/prompt.rb +0 -247
@@ -0,0 +1,59 @@
|
|
1
|
+
# Demo Routes Configuration for FlowChat Comprehensive Demo
|
2
|
+
#
|
3
|
+
# Add these routes to your config/routes.rb file to enable the demo endpoints
|
4
|
+
#
|
5
|
+
# Example integration:
|
6
|
+
# Rails.application.routes.draw do
|
7
|
+
# # Your existing routes...
|
8
|
+
#
|
9
|
+
# # FlowChat Demo Routes
|
10
|
+
# scope :demo do
|
11
|
+
# post 'ussd' => 'demo#ussd_demo'
|
12
|
+
# match 'whatsapp' => 'demo#whatsapp_demo', via: [:get, :post]
|
13
|
+
# match 'whatsapp_custom' => 'demo#whatsapp_custom_demo', via: [:get, :post]
|
14
|
+
# match 'whatsapp_background' => 'demo#whatsapp_background_demo', via: [:get, :post]
|
15
|
+
# match 'whatsapp_simulator' => 'demo#whatsapp_simulator_demo', via: [:get, :post]
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
|
19
|
+
Rails.application.routes.draw do
|
20
|
+
# FlowChat Comprehensive Demo Routes
|
21
|
+
scope :demo do
|
22
|
+
# USSD Demo
|
23
|
+
# Endpoint: POST /demo/ussd
|
24
|
+
# Purpose: Demonstrates USSD integration with all features
|
25
|
+
# Features: Pagination, session management, complex workflows
|
26
|
+
post 'ussd' => 'demo#ussd_demo'
|
27
|
+
|
28
|
+
# WhatsApp Demo (Standard)
|
29
|
+
# Endpoint: GET/POST /demo/whatsapp
|
30
|
+
# Purpose: Standard WhatsApp integration with media support
|
31
|
+
# Features: Rich media, interactive elements, buttons/lists
|
32
|
+
match 'whatsapp' => 'demo#whatsapp_demo', via: [:get, :post]
|
33
|
+
|
34
|
+
# WhatsApp Demo (Custom Configuration)
|
35
|
+
# Endpoint: GET/POST /demo/whatsapp_custom
|
36
|
+
# Purpose: Shows multi-tenant configuration capabilities
|
37
|
+
# Features: Custom credentials, per-endpoint configuration
|
38
|
+
match 'whatsapp_custom' => 'demo#whatsapp_custom_demo', via: [:get, :post]
|
39
|
+
|
40
|
+
# WhatsApp Demo (Background Processing)
|
41
|
+
# Endpoint: GET/POST /demo/whatsapp_background
|
42
|
+
# Purpose: Demonstrates background job integration
|
43
|
+
# Features: Asynchronous response delivery, job queuing
|
44
|
+
match 'whatsapp_background' => 'demo#whatsapp_background_demo', via: [:get, :post]
|
45
|
+
|
46
|
+
# WhatsApp Demo (Simulator Mode)
|
47
|
+
# Endpoint: GET/POST /demo/whatsapp_simulator
|
48
|
+
# Purpose: Testing mode that returns response data as JSON
|
49
|
+
# Features: No actual WhatsApp API calls, perfect for testing
|
50
|
+
match 'whatsapp_simulator' => 'demo#whatsapp_simulator_demo', via: [:get, :post]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Optional: Simulator interface for testing
|
54
|
+
# Uncomment if you want to add the simulator UI
|
55
|
+
# get '/simulator' => 'simulator#index'
|
56
|
+
|
57
|
+
# Optional: API documentation endpoint
|
58
|
+
# get '/demo' => 'demo#index'
|
59
|
+
end
|
data/lib/flow_chat/config.rb
CHANGED
@@ -4,6 +4,9 @@ module FlowChat
|
|
4
4
|
mattr_accessor :logger, default: Logger.new($stdout)
|
5
5
|
mattr_accessor :cache, default: nil
|
6
6
|
mattr_accessor :simulator_secret, default: nil
|
7
|
+
# When true (default), validation errors are combined with the original message.
|
8
|
+
# When false, only the validation error message is shown to the user.
|
9
|
+
mattr_accessor :combine_validation_error_with_message, default: true
|
7
10
|
|
8
11
|
# USSD-specific configuration object
|
9
12
|
def self.ussd
|
data/lib/flow_chat/interrupt.rb
CHANGED
@@ -2,11 +2,12 @@ module FlowChat
|
|
2
2
|
module Interrupt
|
3
3
|
# standard:disable Lint/InheritException
|
4
4
|
class Base < Exception
|
5
|
-
attr_reader :prompt
|
5
|
+
attr_reader :prompt, :media
|
6
6
|
|
7
|
-
def initialize(prompt)
|
7
|
+
def initialize(prompt, media: nil)
|
8
8
|
@prompt = prompt
|
9
|
-
|
9
|
+
@media = media
|
10
|
+
super(prompt)
|
10
11
|
end
|
11
12
|
end
|
12
13
|
# standard:enable Lint/InheritException
|
@@ -14,9 +15,9 @@ module FlowChat
|
|
14
15
|
class Prompt < Base
|
15
16
|
attr_reader :choices
|
16
17
|
|
17
|
-
def initialize(
|
18
|
+
def initialize(prompt, choices: nil, media: nil)
|
18
19
|
@choices = choices
|
19
|
-
super(
|
20
|
+
super(prompt, media: media)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module FlowChat
|
2
|
+
class Prompt
|
3
|
+
attr_reader :user_input
|
4
|
+
|
5
|
+
def initialize(input)
|
6
|
+
@user_input = input
|
7
|
+
end
|
8
|
+
|
9
|
+
def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil, media: nil)
|
10
|
+
# Validate media and choices compatibility
|
11
|
+
validate_media_choices_compatibility(media, choices)
|
12
|
+
|
13
|
+
if user_input.present?
|
14
|
+
input = user_input
|
15
|
+
input = convert.call(input) if convert.present?
|
16
|
+
validation_error = validate.call(input) if validate.present?
|
17
|
+
|
18
|
+
if validation_error.present?
|
19
|
+
# Use config to determine whether to combine validation error with original message
|
20
|
+
message = if FlowChat::Config.combine_validation_error_with_message
|
21
|
+
[validation_error, msg].join("\n\n")
|
22
|
+
else
|
23
|
+
validation_error
|
24
|
+
end
|
25
|
+
prompt!(message, choices: choices, media: media)
|
26
|
+
end
|
27
|
+
|
28
|
+
input = transform.call(input) if transform.present?
|
29
|
+
return input
|
30
|
+
end
|
31
|
+
|
32
|
+
# Pass raw message and media separately to the renderer
|
33
|
+
prompt! msg, choices: choices, media: media
|
34
|
+
end
|
35
|
+
|
36
|
+
def say(message, media: nil)
|
37
|
+
# Pass raw message and media separately to the renderer
|
38
|
+
terminate! message, media: media
|
39
|
+
end
|
40
|
+
|
41
|
+
def select(msg, choices, media: nil)
|
42
|
+
# Validate media and choices compatibility
|
43
|
+
validate_media_choices_compatibility(media, choices)
|
44
|
+
|
45
|
+
choices, choices_prompt = build_select_choices choices
|
46
|
+
ask(
|
47
|
+
msg,
|
48
|
+
choices: choices_prompt,
|
49
|
+
convert: lambda { |choice| choice.to_i },
|
50
|
+
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).cover?(choice) },
|
51
|
+
transform: lambda { |choice| choices[choice - 1] },
|
52
|
+
media: media
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def yes?(msg)
|
57
|
+
select(msg, ["Yes", "No"]) == "Yes"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def validate_media_choices_compatibility(media, choices)
|
63
|
+
return unless media && choices
|
64
|
+
|
65
|
+
if choices.length > 3
|
66
|
+
raise ArgumentError, "Media with more than 3 choices is not supported. Please use either media OR choices for more than 3 options."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_select_choices(choices)
|
71
|
+
case choices
|
72
|
+
when Array
|
73
|
+
choices_prompt = choices.map.with_index { |c, i| [i + 1, c] }.to_h
|
74
|
+
when Hash
|
75
|
+
choices_prompt = choices.values.map.with_index { |c, i| [i + 1, c] }.to_h
|
76
|
+
choices = choices.keys
|
77
|
+
else
|
78
|
+
raise ArgumentError, "choices must be an array or hash"
|
79
|
+
end
|
80
|
+
[choices, choices_prompt]
|
81
|
+
end
|
82
|
+
|
83
|
+
def prompt!(msg, choices: nil, media: nil)
|
84
|
+
raise FlowChat::Interrupt::Prompt.new(msg, choices: choices, media: media)
|
85
|
+
end
|
86
|
+
|
87
|
+
def terminate!(msg, media: nil)
|
88
|
+
raise FlowChat::Interrupt::Terminate.new(msg, media: media)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/flow_chat/ussd/app.rb
CHANGED
@@ -17,7 +17,7 @@ module FlowChat
|
|
17
17
|
navigation_stack << key
|
18
18
|
return session.get(key) if session.get(key).present?
|
19
19
|
|
20
|
-
prompt = FlowChat::
|
20
|
+
prompt = FlowChat::Prompt.new input
|
21
21
|
@input = nil # input is being submitted to prompt so we clear it
|
22
22
|
|
23
23
|
value = yield prompt
|
@@ -25,7 +25,7 @@ module FlowChat
|
|
25
25
|
value
|
26
26
|
end
|
27
27
|
|
28
|
-
def say(msg)
|
28
|
+
def say(msg, media: nil)
|
29
29
|
raise FlowChat::Interrupt::Terminate.new(msg)
|
30
30
|
end
|
31
31
|
|
@@ -20,20 +20,20 @@ module FlowChat
|
|
20
20
|
# context["request.type"] = params["MSGTYPE"] ? :initial : :response
|
21
21
|
context.input = params["USERDATA"].presence
|
22
22
|
|
23
|
-
type, prompt, choices = @app.call(context)
|
23
|
+
type, prompt, choices, media = @app.call(context)
|
24
24
|
|
25
25
|
context.controller.render json: {
|
26
26
|
USERID: params["USERID"],
|
27
27
|
MSISDN: params["MSISDN"],
|
28
|
-
MSG: render_prompt(prompt, choices),
|
28
|
+
MSG: render_prompt(prompt, choices, media),
|
29
29
|
MSGTYPE: type == :prompt
|
30
30
|
}
|
31
31
|
end
|
32
32
|
|
33
33
|
private
|
34
34
|
|
35
|
-
def render_prompt(prompt, choices)
|
36
|
-
FlowChat::Ussd::Renderer.new(prompt, choices).render
|
35
|
+
def render_prompt(prompt, choices, media)
|
36
|
+
FlowChat::Ussd::Renderer.new(prompt, choices: choices, media: media).render
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -11,10 +11,10 @@ module FlowChat
|
|
11
11
|
flow = context.flow.new ussd_app
|
12
12
|
flow.send context["flow.action"]
|
13
13
|
rescue FlowChat::Interrupt::Prompt => e
|
14
|
-
[:prompt, e.prompt, e.choices]
|
14
|
+
[:prompt, e.prompt, e.choices, e.media]
|
15
15
|
rescue FlowChat::Interrupt::Terminate => e
|
16
16
|
context.session.destroy
|
17
|
-
[:terminate, e.prompt, nil]
|
17
|
+
[:terminate, e.prompt, nil, e.media]
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
@@ -12,14 +12,16 @@ module FlowChat
|
|
12
12
|
|
13
13
|
if intercept?
|
14
14
|
type, prompt = handle_intercepted_request
|
15
|
+
[type, prompt, []]
|
15
16
|
else
|
16
17
|
@session.delete "ussd.pagination"
|
17
|
-
type, prompt, choices = @app.call(context)
|
18
|
+
type, prompt, choices, media = @app.call(context)
|
18
19
|
|
19
|
-
prompt = FlowChat::Ussd::Renderer.new(prompt, choices).render
|
20
|
+
prompt = FlowChat::Ussd::Renderer.new(prompt, choices: choices, media: media).render
|
20
21
|
type, prompt = maybe_paginate(type, prompt) if prompt.present?
|
22
|
+
|
23
|
+
[type, prompt, []]
|
21
24
|
end
|
22
|
-
[type, prompt, []]
|
23
25
|
end
|
24
26
|
|
25
27
|
private
|
@@ -34,7 +36,7 @@ module FlowChat
|
|
34
36
|
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
|
35
37
|
start, finish, has_more = calculate_offsets
|
36
38
|
type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
|
37
|
-
prompt = pagination_state["prompt"][start..finish]
|
39
|
+
prompt = pagination_state["prompt"][start..finish] + build_pagination_options(type, has_more)
|
38
40
|
set_pagination_state(current_page, start, finish)
|
39
41
|
|
40
42
|
[type, prompt]
|
@@ -44,11 +46,11 @@ module FlowChat
|
|
44
46
|
if prompt.length > FlowChat::Config.ussd.pagination_page_size
|
45
47
|
original_prompt = prompt
|
46
48
|
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{FlowChat::Config.ussd.pagination_page_size}). Paginating."
|
47
|
-
|
49
|
+
slice_end = single_option_slice_size
|
48
50
|
# Ensure we do not cut words and options off in the middle.
|
49
|
-
current_pagebreak =
|
51
|
+
current_pagebreak = original_prompt[slice_end + 1].blank? ? slice_end : original_prompt[0..slice_end].rindex("\n") || original_prompt[0..slice_end].rindex(" ") || slice_end
|
50
52
|
set_pagination_state(1, 0, current_pagebreak, original_prompt, type)
|
51
|
-
prompt =
|
53
|
+
prompt = original_prompt[0..current_pagebreak] + "\n\n" + next_option
|
52
54
|
type = :prompt
|
53
55
|
end
|
54
56
|
[type, prompt]
|
@@ -72,13 +74,27 @@ module FlowChat
|
|
72
74
|
finish = start + len
|
73
75
|
if start > pagination_state["prompt"].length
|
74
76
|
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
|
77
|
+
page -= 1
|
75
78
|
has_more = false
|
76
79
|
start = previous_offset["start"]
|
77
80
|
finish = previous_offset["finish"]
|
78
81
|
else
|
79
|
-
|
80
|
-
|
81
|
-
finish
|
82
|
+
# Apply word boundary logic for the new page
|
83
|
+
full_prompt = pagination_state["prompt"]
|
84
|
+
if finish < full_prompt.length
|
85
|
+
# Look for word boundary within the slice
|
86
|
+
slice_text = full_prompt[start..finish]
|
87
|
+
# Check if the character after our slice point is a word boundary
|
88
|
+
next_char = full_prompt[finish + 1]
|
89
|
+
if next_char && !next_char.match(/\s/)
|
90
|
+
# We're in the middle of a word, find the last word boundary
|
91
|
+
boundary_pos = slice_text.rindex("\n") || slice_text.rindex(" ")
|
92
|
+
if boundary_pos
|
93
|
+
finish = start + boundary_pos
|
94
|
+
end
|
95
|
+
# If no boundary found, we'll have to break mid-word (fallback)
|
96
|
+
end
|
97
|
+
end
|
82
98
|
end
|
83
99
|
end
|
84
100
|
[start, finish, has_more]
|
@@ -134,21 +150,22 @@ module FlowChat
|
|
134
150
|
end
|
135
151
|
|
136
152
|
def pagination_state
|
137
|
-
@
|
153
|
+
@context.session.get("ussd.pagination") || {}
|
138
154
|
end
|
139
155
|
|
140
156
|
def set_pagination_state(page, offset_start, offset_finish, prompt = nil, type = nil)
|
141
|
-
|
142
|
-
offsets[
|
143
|
-
|
144
|
-
|
145
|
-
|
157
|
+
current_state = pagination_state
|
158
|
+
offsets = current_state["offsets"] || {}
|
159
|
+
offsets[page.to_s] = {"start" => offset_start, "finish" => offset_finish}
|
160
|
+
prompt ||= current_state["prompt"]
|
161
|
+
type ||= current_state["type"]
|
162
|
+
new_state = {
|
146
163
|
"page" => page,
|
147
164
|
"offsets" => offsets,
|
148
165
|
"prompt" => prompt,
|
149
|
-
"type" => type
|
166
|
+
"type" => type.to_s
|
150
167
|
}
|
151
|
-
@session.set "ussd.pagination",
|
168
|
+
@session.set "ussd.pagination", new_state
|
152
169
|
end
|
153
170
|
end
|
154
171
|
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Ussd
|
3
3
|
class Renderer
|
4
|
-
attr_reader :prompt, :choices
|
4
|
+
attr_reader :prompt, :choices, :media
|
5
5
|
|
6
|
-
def initialize(prompt, choices)
|
6
|
+
def initialize(prompt, choices: nil, media: nil)
|
7
7
|
@prompt = prompt
|
8
8
|
@choices = choices
|
9
|
+
@media = media
|
9
10
|
end
|
10
11
|
|
11
12
|
def render = build_prompt
|
@@ -13,7 +14,8 @@ module FlowChat
|
|
13
14
|
private
|
14
15
|
|
15
16
|
def build_prompt
|
16
|
-
[prompt, build_choices].compact
|
17
|
+
parts = [prompt, build_media, build_choices].compact
|
18
|
+
parts.join "\n\n"
|
17
19
|
end
|
18
20
|
|
19
21
|
def build_choices
|
@@ -21,6 +23,29 @@ module FlowChat
|
|
21
23
|
|
22
24
|
choices.map { |i, c| "#{i}. #{c}" }.join "\n"
|
23
25
|
end
|
26
|
+
|
27
|
+
def build_media
|
28
|
+
return unless media.present?
|
29
|
+
|
30
|
+
media_url = media[:url] || media[:path]
|
31
|
+
media_type = media[:type] || :image
|
32
|
+
|
33
|
+
# For USSD, we append the media URL to the message
|
34
|
+
case media_type.to_sym
|
35
|
+
when :image
|
36
|
+
"📷 Image: #{media_url}"
|
37
|
+
when :document
|
38
|
+
"📄 Document: #{media_url}"
|
39
|
+
when :audio
|
40
|
+
"🎵 Audio: #{media_url}"
|
41
|
+
when :video
|
42
|
+
"🎥 Video: #{media_url}"
|
43
|
+
when :sticker
|
44
|
+
"😊 Sticker: #{media_url}"
|
45
|
+
else
|
46
|
+
"📎 Media: #{media_url}"
|
47
|
+
end
|
48
|
+
end
|
24
49
|
end
|
25
50
|
end
|
26
51
|
end
|
data/lib/flow_chat/version.rb
CHANGED
@@ -23,7 +23,7 @@ module FlowChat
|
|
23
23
|
user_input = nil
|
24
24
|
end
|
25
25
|
|
26
|
-
prompt = FlowChat::
|
26
|
+
prompt = FlowChat::Prompt.new user_input
|
27
27
|
@input = nil # input is being submitted to prompt so we clear it
|
28
28
|
|
29
29
|
value = yield prompt
|
@@ -31,8 +31,8 @@ module FlowChat
|
|
31
31
|
value
|
32
32
|
end
|
33
33
|
|
34
|
-
def say(msg)
|
35
|
-
raise FlowChat::Interrupt::Terminate.new(
|
34
|
+
def say(msg, media: nil)
|
35
|
+
raise FlowChat::Interrupt::Terminate.new(msg, media: media)
|
36
36
|
end
|
37
37
|
|
38
38
|
# WhatsApp-specific data accessors (read-only)
|
@@ -192,25 +192,32 @@ module FlowChat
|
|
192
192
|
text: {body: content}
|
193
193
|
}
|
194
194
|
when :interactive_buttons
|
195
|
+
interactive_payload = {
|
196
|
+
type: "button",
|
197
|
+
body: {text: content},
|
198
|
+
action: {
|
199
|
+
buttons: options[:buttons].map.with_index do |button, index|
|
200
|
+
{
|
201
|
+
type: "reply",
|
202
|
+
reply: {
|
203
|
+
id: button[:id] || index.to_s,
|
204
|
+
title: button[:title]
|
205
|
+
}
|
206
|
+
}
|
207
|
+
end
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
# Add header if provided (for media support)
|
212
|
+
if options[:header]
|
213
|
+
interactive_payload[:header] = options[:header]
|
214
|
+
end
|
215
|
+
|
195
216
|
{
|
196
217
|
messaging_product: "whatsapp",
|
197
218
|
to: to,
|
198
219
|
type: "interactive",
|
199
|
-
interactive:
|
200
|
-
type: "button",
|
201
|
-
body: {text: content},
|
202
|
-
action: {
|
203
|
-
buttons: options[:buttons].map.with_index do |button, index|
|
204
|
-
{
|
205
|
-
type: "reply",
|
206
|
-
reply: {
|
207
|
-
id: button[:id] || index.to_s,
|
208
|
-
title: button[:title]
|
209
|
-
}
|
210
|
-
}
|
211
|
-
end
|
212
|
-
}
|
213
|
-
}
|
220
|
+
interactive: interactive_payload
|
214
221
|
}
|
215
222
|
when :interactive_list
|
216
223
|
{
|
@@ -114,14 +114,26 @@ module FlowChat
|
|
114
114
|
handler_mode = determine_message_handler(context)
|
115
115
|
|
116
116
|
# Process the message based on handling mode
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
117
|
+
begin
|
118
|
+
case handler_mode
|
119
|
+
when :inline
|
120
|
+
handle_message_inline(context, controller)
|
121
|
+
when :background
|
122
|
+
handle_message_background(context, controller)
|
123
|
+
when :simulator
|
124
|
+
# Return early from simulator mode to preserve the JSON response
|
125
|
+
return handle_message_simulator(context, controller)
|
126
|
+
end
|
127
|
+
rescue => e
|
128
|
+
# Log the error and set appropriate HTTP status
|
129
|
+
Rails.logger.error "Error processing WhatsApp message: #{e.message}"
|
130
|
+
Rails.logger.error e.backtrace&.join("\n") if e.backtrace
|
131
|
+
|
132
|
+
# Return error status to WhatsApp so they know processing failed
|
133
|
+
controller.head :internal_server_error
|
134
|
+
|
135
|
+
# Re-raise the error to bubble it up for proper error tracking/monitoring
|
136
|
+
raise e
|
125
137
|
end
|
126
138
|
end
|
127
139
|
|
@@ -217,7 +229,9 @@ module FlowChat
|
|
217
229
|
def handle_message_inline(context, controller)
|
218
230
|
response = @app.call(context)
|
219
231
|
if response
|
220
|
-
|
232
|
+
_type, prompt, choices, media = response
|
233
|
+
rendered_message = render_response(prompt, choices, media)
|
234
|
+
result = @client.send_message(context["request.msisdn"], rendered_message)
|
221
235
|
context["whatsapp.message_result"] = result
|
222
236
|
end
|
223
237
|
end
|
@@ -227,10 +241,13 @@ module FlowChat
|
|
227
241
|
response = @app.call(context)
|
228
242
|
|
229
243
|
if response
|
244
|
+
_type, prompt, choices, media = response
|
245
|
+
rendered_message = render_response(prompt, choices, media)
|
246
|
+
|
230
247
|
# Queue only the response delivery asynchronously
|
231
248
|
send_data = {
|
232
249
|
msisdn: context["request.msisdn"],
|
233
|
-
response:
|
250
|
+
response: rendered_message,
|
234
251
|
config_name: @config.name
|
235
252
|
}
|
236
253
|
|
@@ -244,7 +261,7 @@ module FlowChat
|
|
244
261
|
rescue NameError
|
245
262
|
# Fallback to inline sending if no job system
|
246
263
|
Rails.logger.warn "Background mode requested but no #{job_class_name} found. Falling back to inline sending."
|
247
|
-
result = @client.send_message(context["request.msisdn"],
|
264
|
+
result = @client.send_message(context["request.msisdn"], rendered_message)
|
248
265
|
context["whatsapp.message_result"] = result
|
249
266
|
end
|
250
267
|
end
|
@@ -254,9 +271,12 @@ module FlowChat
|
|
254
271
|
response = @app.call(context)
|
255
272
|
|
256
273
|
if response
|
274
|
+
_type, prompt, choices, media = response
|
275
|
+
rendered_message = render_response(prompt, choices, media)
|
276
|
+
|
257
277
|
# For simulator mode, return the response data in the HTTP response
|
258
278
|
# instead of actually sending via WhatsApp API
|
259
|
-
message_payload = @client.build_message_payload(
|
279
|
+
message_payload = @client.build_message_payload(rendered_message, context["request.msisdn"])
|
260
280
|
|
261
281
|
simulator_response = {
|
262
282
|
mode: "simulator",
|
@@ -317,6 +337,10 @@ module FlowChat
|
|
317
337
|
def parse_request_body(request)
|
318
338
|
@body ||= JSON.parse(request.body.read)
|
319
339
|
end
|
340
|
+
|
341
|
+
def render_response(prompt, choices, media)
|
342
|
+
FlowChat::Whatsapp::Renderer.new(prompt, choices: choices, media: media).render
|
343
|
+
end
|
320
344
|
end
|
321
345
|
end
|
322
346
|
end
|
@@ -11,12 +11,12 @@ module FlowChat
|
|
11
11
|
flow = context.flow.new whatsapp_app
|
12
12
|
flow.send context["flow.action"]
|
13
13
|
rescue FlowChat::Interrupt::Prompt => e
|
14
|
-
# Return the
|
15
|
-
e.prompt
|
14
|
+
# Return the same triplet format as USSD for consistency
|
15
|
+
[:prompt, e.prompt, e.choices, e.media]
|
16
16
|
rescue FlowChat::Interrupt::Terminate => e
|
17
17
|
# Clean up session and return terminal message
|
18
18
|
context.session.destroy
|
19
|
-
e.prompt
|
19
|
+
[:terminate, e.prompt, nil, e.media]
|
20
20
|
end
|
21
21
|
|
22
22
|
private
|