flow_chat 0.4.1 → 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/examples/initializer.rb +1 -1
- data/examples/media_prompts_examples.rb +1 -2
- data/examples/multi_tenant_whatsapp_controller.rb +56 -56
- data/examples/ussd_controller.rb +17 -11
- data/examples/whatsapp_controller.rb +10 -10
- data/examples/whatsapp_media_examples.rb +78 -80
- data/examples/whatsapp_message_job.rb +3 -3
- data/lib/flow_chat/base_processor.rb +1 -1
- data/lib/flow_chat/config.rb +4 -3
- data/lib/flow_chat/session/cache_session_store.rb +5 -5
- data/lib/flow_chat/simulator/views/simulator.html.erb +287 -12
- data/lib/flow_chat/ussd/gateway/nsano.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +1 -1
- data/lib/flow_chat/ussd/prompt.rb +13 -13
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +41 -45
- data/lib/flow_chat/whatsapp/configuration.rb +10 -10
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +8 -10
- data/lib/flow_chat/whatsapp/middleware/executor.rb +1 -1
- data/lib/flow_chat/whatsapp/processor.rb +1 -1
- data/lib/flow_chat/whatsapp/prompt.rb +27 -31
- data/lib/flow_chat/whatsapp/send_job_support.rb +7 -7
- data/lib/flow_chat/whatsapp/template_manager.rb +7 -7
- metadata +1 -1
@@ -2,7 +2,7 @@ module FlowChat
|
|
2
2
|
module Whatsapp
|
3
3
|
class Configuration
|
4
4
|
attr_accessor :access_token, :phone_number_id, :verify_token, :app_id, :app_secret,
|
5
|
-
|
5
|
+
:webhook_url, :webhook_verify_token, :business_account_id, :name
|
6
6
|
|
7
7
|
# Class-level storage for named configurations
|
8
8
|
@@configurations = {}
|
@@ -24,7 +24,7 @@ module FlowChat
|
|
24
24
|
# Load configuration from Rails credentials or environment variables
|
25
25
|
def self.from_credentials
|
26
26
|
config = new(nil)
|
27
|
-
|
27
|
+
|
28
28
|
if defined?(Rails) && Rails.application.credentials.whatsapp
|
29
29
|
credentials = Rails.application.credentials.whatsapp
|
30
30
|
config.access_token = credentials[:access_token]
|
@@ -36,13 +36,13 @@ module FlowChat
|
|
36
36
|
config.business_account_id = credentials[:business_account_id]
|
37
37
|
else
|
38
38
|
# Fallback to environment variables
|
39
|
-
config.access_token = ENV[
|
40
|
-
config.phone_number_id = ENV[
|
41
|
-
config.verify_token = ENV[
|
42
|
-
config.app_id = ENV[
|
43
|
-
config.app_secret = ENV[
|
44
|
-
config.webhook_url = ENV[
|
45
|
-
config.business_account_id = ENV[
|
39
|
+
config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
|
40
|
+
config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
|
41
|
+
config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
|
42
|
+
config.app_id = ENV["WHATSAPP_APP_ID"]
|
43
|
+
config.app_secret = ENV["WHATSAPP_APP_SECRET"]
|
44
|
+
config.webhook_url = ENV["WHATSAPP_WEBHOOK_URL"]
|
45
|
+
config.business_account_id = ENV["WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
46
46
|
end
|
47
47
|
|
48
48
|
config
|
@@ -110,4 +110,4 @@ module FlowChat
|
|
110
110
|
end
|
111
111
|
end
|
112
112
|
end
|
113
|
-
end
|
113
|
+
end
|
@@ -32,9 +32,7 @@ module FlowChat
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# Expose client for out-of-band messaging
|
35
|
-
|
36
|
-
@client
|
37
|
-
end
|
35
|
+
attr_reader :client
|
38
36
|
|
39
37
|
private
|
40
38
|
|
@@ -53,7 +51,7 @@ module FlowChat
|
|
53
51
|
params = controller.request.params
|
54
52
|
|
55
53
|
verify_token = @config.verify_token
|
56
|
-
|
54
|
+
|
57
55
|
if params["hub.verify_token"] == verify_token
|
58
56
|
controller.render plain: params["hub.challenge"]
|
59
57
|
else
|
@@ -159,7 +157,7 @@ module FlowChat
|
|
159
157
|
def handle_message_background(context, controller)
|
160
158
|
# Process the flow synchronously (maintaining controller context)
|
161
159
|
response = @app.call(context)
|
162
|
-
|
160
|
+
|
163
161
|
if response
|
164
162
|
# Queue only the response delivery asynchronously
|
165
163
|
send_data = {
|
@@ -170,7 +168,7 @@ module FlowChat
|
|
170
168
|
|
171
169
|
# Get job class from configuration
|
172
170
|
job_class_name = FlowChat::Config.whatsapp.background_job_class
|
173
|
-
|
171
|
+
|
174
172
|
# Enqueue background job for sending only
|
175
173
|
begin
|
176
174
|
job_class = job_class_name.constantize
|
@@ -186,12 +184,12 @@ module FlowChat
|
|
186
184
|
|
187
185
|
def handle_message_simulator(context, controller)
|
188
186
|
response = @app.call(context)
|
189
|
-
|
187
|
+
|
190
188
|
if response
|
191
189
|
# For simulator mode, return the response data in the HTTP response
|
192
190
|
# instead of actually sending via WhatsApp API
|
193
191
|
message_payload = @client.build_message_payload(response, context["request.msisdn"])
|
194
|
-
|
192
|
+
|
195
193
|
simulator_response = {
|
196
194
|
mode: "simulator",
|
197
195
|
webhook_processed: true,
|
@@ -204,10 +202,10 @@ module FlowChat
|
|
204
202
|
}
|
205
203
|
|
206
204
|
controller.render json: simulator_response
|
207
|
-
|
205
|
+
nil
|
208
206
|
end
|
209
207
|
end
|
210
208
|
end
|
211
209
|
end
|
212
210
|
end
|
213
|
-
end
|
211
|
+
end
|
@@ -57,10 +57,10 @@ module FlowChat
|
|
57
57
|
end
|
58
58
|
|
59
59
|
buttons = [
|
60
|
-
{
|
61
|
-
{
|
60
|
+
{id: "yes", title: "Yes"},
|
61
|
+
{id: "no", title: "No"}
|
62
62
|
]
|
63
|
-
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, {
|
63
|
+
raise FlowChat::Interrupt::Prompt.new([:interactive_buttons, message, {buttons: buttons}])
|
64
64
|
end
|
65
65
|
|
66
66
|
private
|
@@ -72,15 +72,15 @@ module FlowChat
|
|
72
72
|
|
73
73
|
case media_type.to_sym
|
74
74
|
when :image
|
75
|
-
[:media_image, "", {
|
75
|
+
[:media_image, "", {url: url, caption: message}]
|
76
76
|
when :document
|
77
|
-
[:media_document, "", {
|
77
|
+
[:media_document, "", {url: url, caption: message, filename: filename}]
|
78
78
|
when :audio
|
79
|
-
[:media_audio, "", {
|
79
|
+
[:media_audio, "", {url: url, caption: message}]
|
80
80
|
when :video
|
81
|
-
[:media_video, "", {
|
81
|
+
[:media_video, "", {url: url, caption: message}]
|
82
82
|
when :sticker
|
83
|
-
[:media_sticker, "", {
|
83
|
+
[:media_sticker, "", {url: url}] # Stickers don't support captions
|
84
84
|
else
|
85
85
|
raise ArgumentError, "Unsupported media type: #{media_type}"
|
86
86
|
end
|
@@ -113,21 +113,19 @@ module FlowChat
|
|
113
113
|
}
|
114
114
|
end
|
115
115
|
|
116
|
-
[:interactive_buttons, message, {
|
116
|
+
[:interactive_buttons, message, {buttons: buttons}]
|
117
117
|
end
|
118
118
|
|
119
119
|
def build_list_prompt(message, choices)
|
120
120
|
items = choices.map do |key, value|
|
121
121
|
original_text = value.to_s
|
122
122
|
truncated_title = truncate_text(original_text, 24)
|
123
|
-
|
123
|
+
|
124
124
|
# If title was truncated, put full text in description (up to 72 chars)
|
125
125
|
description = if original_text.length > 24
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
end
|
130
|
-
|
126
|
+
truncate_text(original_text, 72)
|
127
|
+
end
|
128
|
+
|
131
129
|
{
|
132
130
|
id: key.to_s,
|
133
131
|
title: truncated_title,
|
@@ -136,8 +134,8 @@ module FlowChat
|
|
136
134
|
end
|
137
135
|
|
138
136
|
# If 10 or fewer items, use single section
|
139
|
-
if items.length <= 10
|
140
|
-
|
137
|
+
sections = if items.length <= 10
|
138
|
+
[
|
141
139
|
{
|
142
140
|
title: "Options",
|
143
141
|
rows: items
|
@@ -145,10 +143,10 @@ module FlowChat
|
|
145
143
|
]
|
146
144
|
else
|
147
145
|
# Paginate into multiple sections (max 10 items per section)
|
148
|
-
|
146
|
+
items.each_slice(10).with_index.map do |section_items, index|
|
149
147
|
start_num = (index * 10) + 1
|
150
148
|
end_num = start_num + section_items.length - 1
|
151
|
-
|
149
|
+
|
152
150
|
{
|
153
151
|
title: "#{start_num}-#{end_num}",
|
154
152
|
rows: section_items
|
@@ -156,7 +154,7 @@ module FlowChat
|
|
156
154
|
end
|
157
155
|
end
|
158
156
|
|
159
|
-
[:interactive_list, message, {
|
157
|
+
[:interactive_list, message, {sections: sections}]
|
160
158
|
end
|
161
159
|
|
162
160
|
def process_input(input, transform, validate, convert)
|
@@ -178,8 +176,8 @@ module FlowChat
|
|
178
176
|
end
|
179
177
|
|
180
178
|
def process_selection(input, choices, transform, validate, convert)
|
181
|
-
choice_hash = choices.is_a?(Array) ?
|
182
|
-
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] } :
|
183
181
|
choices
|
184
182
|
|
185
183
|
# Check if input matches a valid choice
|
@@ -199,13 +197,11 @@ module FlowChat
|
|
199
197
|
|
200
198
|
def process_boolean(input, transform, validate, convert)
|
201
199
|
boolean_value = case input.to_s.downcase
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
nil
|
208
|
-
end
|
200
|
+
when "yes", "y", "1", "true"
|
201
|
+
true
|
202
|
+
when "no", "n", "0", "false"
|
203
|
+
false
|
204
|
+
end
|
209
205
|
|
210
206
|
if boolean_value.nil?
|
211
207
|
raise FlowChat::Interrupt::Prompt.new([:text, "Please answer with Yes or No.", {}])
|
@@ -220,7 +216,7 @@ module FlowChat
|
|
220
216
|
raise ArgumentError, "choices cannot be empty"
|
221
217
|
end
|
222
218
|
|
223
|
-
choice_count = choices.
|
219
|
+
choice_count = choices.length
|
224
220
|
|
225
221
|
# WhatsApp supports max 100 total items across all sections
|
226
222
|
if choice_count > 100
|
@@ -248,4 +244,4 @@ module FlowChat
|
|
248
244
|
end
|
249
245
|
end
|
250
246
|
end
|
251
|
-
end
|
247
|
+
end
|
@@ -15,11 +15,11 @@ module FlowChat
|
|
15
15
|
def perform_whatsapp_send(send_data)
|
16
16
|
config = resolve_whatsapp_config(send_data)
|
17
17
|
client = FlowChat::Whatsapp::Client.new(config)
|
18
|
-
|
18
|
+
|
19
19
|
result = client.send_message(send_data[:msisdn], send_data[:response])
|
20
|
-
|
20
|
+
|
21
21
|
if result
|
22
|
-
Rails.logger.info "WhatsApp message sent successfully: #{result[
|
22
|
+
Rails.logger.info "WhatsApp message sent successfully: #{result["messages"]&.first&.dig("id")}"
|
23
23
|
on_whatsapp_send_success(send_data, result)
|
24
24
|
else
|
25
25
|
Rails.logger.error "Failed to send WhatsApp message to #{send_data[:msisdn]}"
|
@@ -47,20 +47,20 @@ module FlowChat
|
|
47
47
|
def handle_whatsapp_send_error(error, send_data, config = nil)
|
48
48
|
Rails.logger.error "WhatsApp send job error: #{error.message}"
|
49
49
|
Rails.logger.error error.backtrace&.join("\n") if error.backtrace
|
50
|
-
|
50
|
+
|
51
51
|
# Try to send error message to user if we have config
|
52
52
|
if config
|
53
53
|
begin
|
54
54
|
client = FlowChat::Whatsapp::Client.new(config)
|
55
55
|
client.send_text(
|
56
|
-
send_data[:msisdn],
|
56
|
+
send_data[:msisdn],
|
57
57
|
"⚠️ We're experiencing technical difficulties. Please try again in a few minutes."
|
58
58
|
)
|
59
59
|
rescue => send_error
|
60
60
|
Rails.logger.error "Failed to send error message: #{send_error.message}"
|
61
61
|
end
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
# Re-raise for job retry logic
|
65
65
|
raise error
|
66
66
|
end
|
@@ -76,4 +76,4 @@ module FlowChat
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
end
|
79
|
-
end
|
79
|
+
end
|
@@ -16,7 +16,7 @@ module FlowChat
|
|
16
16
|
type: "template",
|
17
17
|
template: {
|
18
18
|
name: template_name,
|
19
|
-
language: {
|
19
|
+
language: {code: language},
|
20
20
|
components: components
|
21
21
|
}
|
22
22
|
}
|
@@ -27,7 +27,7 @@ module FlowChat
|
|
27
27
|
# Common template structures
|
28
28
|
def send_welcome_template(to:, name: nil)
|
29
29
|
components = []
|
30
|
-
|
30
|
+
|
31
31
|
if name
|
32
32
|
components << {
|
33
33
|
type: "header",
|
@@ -87,7 +87,7 @@ module FlowChat
|
|
87
87
|
def create_template(name:, category:, language: "en_US", components: [])
|
88
88
|
business_account_id = @config.business_account_id
|
89
89
|
uri = URI("https://graph.facebook.com/v18.0/#{business_account_id}/message_templates")
|
90
|
-
|
90
|
+
|
91
91
|
template_data = {
|
92
92
|
name: name,
|
93
93
|
category: category, # AUTHENTICATION, MARKETING, UTILITY
|
@@ -111,7 +111,7 @@ module FlowChat
|
|
111
111
|
def list_templates
|
112
112
|
business_account_id = @config.business_account_id
|
113
113
|
uri = URI("https://graph.facebook.com/v18.0/#{business_account_id}/message_templates")
|
114
|
-
|
114
|
+
|
115
115
|
http = Net::HTTP.new(uri.host, uri.port)
|
116
116
|
http.use_ssl = true
|
117
117
|
|
@@ -125,7 +125,7 @@ module FlowChat
|
|
125
125
|
# Get template status
|
126
126
|
def template_status(template_id)
|
127
127
|
uri = URI("https://graph.facebook.com/v18.0/#{template_id}")
|
128
|
-
|
128
|
+
|
129
129
|
http = Net::HTTP.new(uri.host, uri.port)
|
130
130
|
http.use_ssl = true
|
131
131
|
|
@@ -149,7 +149,7 @@ module FlowChat
|
|
149
149
|
request.body = message_data.to_json
|
150
150
|
|
151
151
|
response = http.request(request)
|
152
|
-
|
152
|
+
|
153
153
|
unless response.is_a?(Net::HTTPSuccess)
|
154
154
|
Rails.logger.error "WhatsApp Template API error: #{response.body}"
|
155
155
|
return nil
|
@@ -159,4 +159,4 @@ module FlowChat
|
|
159
159
|
end
|
160
160
|
end
|
161
161
|
end
|
162
|
-
end
|
162
|
+
end
|