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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +408 -102
- data/examples/media_prompts_examples.rb +28 -0
- data/examples/multi_tenant_whatsapp_controller.rb +4 -8
- data/examples/whatsapp_controller.rb +1 -2
- data/examples/whatsapp_media_examples.rb +406 -0
- data/examples/whatsapp_message_job.rb +111 -0
- data/lib/flow_chat/base_processor.rb +7 -3
- data/lib/flow_chat/config.rb +36 -0
- data/lib/flow_chat/simulator/controller.rb +78 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
- data/lib/flow_chat/ussd/processor.rb +0 -1
- data/lib/flow_chat/ussd/prompt.rb +39 -5
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +7 -1
- data/lib/flow_chat/whatsapp/client.rb +439 -0
- data/lib/flow_chat/whatsapp/configuration.rb +41 -3
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +114 -114
- data/lib/flow_chat/whatsapp/processor.rb +0 -10
- data/lib/flow_chat/whatsapp/prompt.rb +118 -73
- data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
- 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,8 +31,23 @@ module FlowChat
|
|
30
31
|
controller.head :bad_request
|
31
32
|
end
|
32
33
|
|
34
|
+
# Expose client for out-of-band messaging
|
35
|
+
def client
|
36
|
+
@client
|
37
|
+
end
|
38
|
+
|
33
39
|
private
|
34
40
|
|
41
|
+
def determine_message_handler(context)
|
42
|
+
# Check for simulator parameter in request (highest priority)
|
43
|
+
if context["simulator_mode"] || context.controller.request.params["simulator_mode"]
|
44
|
+
return :simulator
|
45
|
+
end
|
46
|
+
|
47
|
+
# Use global WhatsApp configuration
|
48
|
+
FlowChat::Config.whatsapp.message_handling_mode
|
49
|
+
end
|
50
|
+
|
35
51
|
def handle_verification(context)
|
36
52
|
controller = context.controller
|
37
53
|
params = controller.request.params
|
@@ -49,6 +65,11 @@ module FlowChat
|
|
49
65
|
controller = context.controller
|
50
66
|
body = JSON.parse(controller.request.body.read)
|
51
67
|
|
68
|
+
# Check for simulator mode parameter in request
|
69
|
+
if body.dig("simulator_mode") || controller.request.params["simulator_mode"]
|
70
|
+
context["simulator_mode"] = true
|
71
|
+
end
|
72
|
+
|
52
73
|
# Extract message data from WhatsApp webhook
|
53
74
|
entry = body.dig("entry", 0)
|
54
75
|
return controller.head :ok unless entry
|
@@ -72,139 +93,118 @@ module FlowChat
|
|
72
93
|
context["request.timestamp"] = message["timestamp"]
|
73
94
|
|
74
95
|
# 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$"
|
96
|
+
extract_message_content(message, context)
|
97
|
+
|
98
|
+
# Determine message handling mode
|
99
|
+
handler_mode = determine_message_handler(context)
|
100
|
+
|
101
|
+
# Process the message based on handling mode
|
102
|
+
case handler_mode
|
103
|
+
when :inline
|
104
|
+
handle_message_inline(context, controller)
|
105
|
+
when :background
|
106
|
+
handle_message_background(context, controller)
|
107
|
+
when :simulator
|
108
|
+
# Return early from simulator mode to preserve the JSON response
|
109
|
+
return handle_message_simulator(context, controller)
|
103
110
|
end
|
104
|
-
|
105
|
-
response = @app.call(context)
|
106
|
-
send_whatsapp_message(context, response)
|
107
111
|
end
|
108
112
|
|
109
113
|
# Handle message status updates
|
110
114
|
if value["statuses"]&.any?
|
111
|
-
# Log status updates but don't process them
|
112
115
|
Rails.logger.info "WhatsApp status update: #{value["statuses"]}"
|
113
116
|
end
|
114
117
|
|
115
118
|
controller.head :ok
|
116
119
|
end
|
117
120
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
121
|
+
def extract_message_content(message, context)
|
122
|
+
case message["type"]
|
123
|
+
when "text"
|
124
|
+
context.input = message.dig("text", "body")
|
125
|
+
when "interactive"
|
126
|
+
# Handle button/list replies
|
127
|
+
if message.dig("interactive", "type") == "button_reply"
|
128
|
+
context.input = message.dig("interactive", "button_reply", "id")
|
129
|
+
elsif message.dig("interactive", "type") == "list_reply"
|
130
|
+
context.input = message.dig("interactive", "list_reply", "id")
|
131
|
+
end
|
132
|
+
when "location"
|
133
|
+
context["request.location"] = {
|
134
|
+
latitude: message.dig("location", "latitude"),
|
135
|
+
longitude: message.dig("location", "longitude"),
|
136
|
+
name: message.dig("location", "name"),
|
137
|
+
address: message.dig("location", "address")
|
138
|
+
}
|
139
|
+
context.input = "$location$"
|
140
|
+
when "image", "document", "audio", "video"
|
141
|
+
context["request.media"] = {
|
142
|
+
type: message["type"],
|
143
|
+
id: message.dig(message["type"], "id"),
|
144
|
+
mime_type: message.dig(message["type"], "mime_type"),
|
145
|
+
caption: message.dig(message["type"], "caption")
|
146
|
+
}
|
147
|
+
context.input = "$media$"
|
148
|
+
end
|
149
|
+
end
|
130
150
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
151
|
+
def handle_message_inline(context, controller)
|
152
|
+
response = @app.call(context)
|
153
|
+
if response
|
154
|
+
result = @client.send_message(context["request.msisdn"], response)
|
155
|
+
context["whatsapp.message_result"] = result
|
156
|
+
end
|
157
|
+
end
|
135
158
|
|
136
|
-
|
159
|
+
def handle_message_background(context, controller)
|
160
|
+
# Process the flow synchronously (maintaining controller context)
|
161
|
+
response = @app.call(context)
|
137
162
|
|
138
|
-
|
139
|
-
|
163
|
+
if response
|
164
|
+
# Queue only the response delivery asynchronously
|
165
|
+
send_data = {
|
166
|
+
msisdn: context["request.msisdn"],
|
167
|
+
response: response,
|
168
|
+
config_name: @config.name
|
169
|
+
}
|
170
|
+
|
171
|
+
# Get job class from configuration
|
172
|
+
job_class_name = FlowChat::Config.whatsapp.background_job_class
|
173
|
+
|
174
|
+
# Enqueue background job for sending only
|
175
|
+
begin
|
176
|
+
job_class = job_class_name.constantize
|
177
|
+
job_class.perform_later(send_data)
|
178
|
+
rescue NameError
|
179
|
+
# Fallback to inline sending if no job system
|
180
|
+
Rails.logger.warn "Background mode requested but no #{job_class_name} found. Falling back to inline sending."
|
181
|
+
result = @client.send_message(context["request.msisdn"], response)
|
182
|
+
context["whatsapp.message_result"] = result
|
183
|
+
end
|
140
184
|
end
|
141
185
|
end
|
142
186
|
|
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
|
-
}
|
173
|
-
}
|
174
|
-
}
|
175
|
-
when :interactive_list
|
176
|
-
{
|
177
|
-
messaging_product: "whatsapp",
|
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] || []
|
187
|
+
def handle_message_simulator(context, controller)
|
188
|
+
response = @app.call(context)
|
189
|
+
|
190
|
+
if response
|
191
|
+
# For simulator mode, return the response data in the HTTP response
|
192
|
+
# instead of actually sending via WhatsApp API
|
193
|
+
message_payload = @client.build_message_payload(response, context["request.msisdn"])
|
194
|
+
|
195
|
+
simulator_response = {
|
196
|
+
mode: "simulator",
|
197
|
+
webhook_processed: true,
|
198
|
+
would_send: message_payload,
|
199
|
+
message_info: {
|
200
|
+
to: context["request.msisdn"],
|
201
|
+
contact_name: context["request.contact_name"],
|
202
|
+
timestamp: Time.now.iso8601
|
198
203
|
}
|
199
204
|
}
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
messaging_product: "whatsapp",
|
204
|
-
to: to,
|
205
|
-
type: "text",
|
206
|
-
text: { body: content.to_s }
|
207
|
-
}
|
205
|
+
|
206
|
+
controller.render json: simulator_response
|
207
|
+
return
|
208
208
|
end
|
209
209
|
end
|
210
210
|
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,7 +17,6 @@ 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
|
@@ -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
|
14
21
|
end
|
15
22
|
|
16
|
-
# Send message and wait for response
|
17
|
-
|
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, {}])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
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,119 @@ 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
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
{
|
132
|
+
id: key.to_s,
|
133
|
+
title: truncated_title,
|
134
|
+
description: description
|
135
|
+
}.compact
|
136
|
+
end
|
137
|
+
|
138
|
+
# If 10 or fewer items, use single section
|
139
|
+
if items.length <= 10
|
140
|
+
sections = [
|
141
|
+
{
|
142
|
+
title: "Options",
|
143
|
+
rows: items
|
144
|
+
}
|
145
|
+
]
|
146
|
+
else
|
147
|
+
# Paginate into multiple sections (max 10 items per section)
|
148
|
+
sections = items.each_slice(10).with_index.map do |section_items, index|
|
149
|
+
start_num = (index * 10) + 1
|
150
|
+
end_num = start_num + section_items.length - 1
|
151
|
+
|
152
|
+
{
|
153
|
+
title: "#{start_num}-#{end_num}",
|
154
|
+
rows: section_items
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
[:interactive_list, message, { sections: sections }]
|
160
|
+
end
|
62
161
|
|
63
162
|
def process_input(input, transform, validate, convert)
|
64
163
|
# Apply transformation
|
@@ -115,60 +214,6 @@ module FlowChat
|
|
115
214
|
process_input(boolean_value, transform, validate, convert)
|
116
215
|
end
|
117
216
|
|
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
217
|
def validate_choices(choices)
|
173
218
|
# Check for empty choices
|
174
219
|
if choices.nil? || choices.empty?
|
@@ -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
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow_chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
@@ -98,9 +98,12 @@ files:
|
|
98
98
|
- bin/console
|
99
99
|
- bin/setup
|
100
100
|
- examples/initializer.rb
|
101
|
+
- examples/media_prompts_examples.rb
|
101
102
|
- examples/multi_tenant_whatsapp_controller.rb
|
102
103
|
- examples/ussd_controller.rb
|
103
104
|
- examples/whatsapp_controller.rb
|
105
|
+
- examples/whatsapp_media_examples.rb
|
106
|
+
- examples/whatsapp_message_job.rb
|
104
107
|
- flow_chat.gemspec
|
105
108
|
- images/ussd_simulator.png
|
106
109
|
- lib/flow_chat.rb
|
@@ -112,6 +115,8 @@ files:
|
|
112
115
|
- lib/flow_chat/session/cache_session_store.rb
|
113
116
|
- lib/flow_chat/session/middleware.rb
|
114
117
|
- lib/flow_chat/session/rails_session_store.rb
|
118
|
+
- lib/flow_chat/simulator/controller.rb
|
119
|
+
- lib/flow_chat/simulator/views/simulator.html.erb
|
115
120
|
- lib/flow_chat/ussd/app.rb
|
116
121
|
- lib/flow_chat/ussd/gateway/nalo.rb
|
117
122
|
- lib/flow_chat/ussd/gateway/nsano.rb
|
@@ -121,15 +126,15 @@ files:
|
|
121
126
|
- lib/flow_chat/ussd/processor.rb
|
122
127
|
- lib/flow_chat/ussd/prompt.rb
|
123
128
|
- lib/flow_chat/ussd/renderer.rb
|
124
|
-
- lib/flow_chat/ussd/simulator/controller.rb
|
125
|
-
- lib/flow_chat/ussd/simulator/views/simulator.html.erb
|
126
129
|
- lib/flow_chat/version.rb
|
127
130
|
- lib/flow_chat/whatsapp/app.rb
|
131
|
+
- lib/flow_chat/whatsapp/client.rb
|
128
132
|
- lib/flow_chat/whatsapp/configuration.rb
|
129
133
|
- lib/flow_chat/whatsapp/gateway/cloud_api.rb
|
130
134
|
- lib/flow_chat/whatsapp/middleware/executor.rb
|
131
135
|
- lib/flow_chat/whatsapp/processor.rb
|
132
136
|
- lib/flow_chat/whatsapp/prompt.rb
|
137
|
+
- lib/flow_chat/whatsapp/send_job_support.rb
|
133
138
|
- lib/flow_chat/whatsapp/template_manager.rb
|
134
139
|
homepage: https://github.com/radioactive-labs/flow_chat
|
135
140
|
licenses:
|