flow_chat 0.4.1 → 0.5.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/README.md +418 -295
- data/SECURITY.md +365 -0
- data/examples/initializer.rb +56 -1
- data/examples/media_prompts_examples.rb +1 -2
- data/examples/multi_tenant_whatsapp_controller.rb +61 -57
- data/examples/simulator_controller.rb +95 -0
- data/examples/ussd_controller.rb +17 -11
- data/examples/whatsapp_controller.rb +103 -14
- data/examples/whatsapp_media_examples.rb +78 -80
- data/examples/whatsapp_message_job.rb +3 -3
- data/lib/flow_chat/base_processor.rb +3 -2
- data/lib/flow_chat/config.rb +6 -3
- data/lib/flow_chat/session/cache_session_store.rb +5 -5
- data/lib/flow_chat/simulator/controller.rb +34 -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 +44 -50
- data/lib/flow_chat/whatsapp/configuration.rb +21 -20
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +129 -19
- 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 +10 -10
- metadata +4 -2
@@ -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",
|
@@ -86,8 +86,8 @@ module FlowChat
|
|
86
86
|
# Create a new template (requires approval from Meta)
|
87
87
|
def create_template(name:, category:, language: "en_US", components: [])
|
88
88
|
business_account_id = @config.business_account_id
|
89
|
-
uri = URI("
|
90
|
-
|
89
|
+
uri = URI("#{FlowChat::Config.whatsapp.api_base_url}/#{business_account_id}/message_templates")
|
90
|
+
|
91
91
|
template_data = {
|
92
92
|
name: name,
|
93
93
|
category: category, # AUTHENTICATION, MARKETING, UTILITY
|
@@ -110,8 +110,8 @@ module FlowChat
|
|
110
110
|
# List all templates
|
111
111
|
def list_templates
|
112
112
|
business_account_id = @config.business_account_id
|
113
|
-
uri = URI("
|
114
|
-
|
113
|
+
uri = URI("#{FlowChat::Config.whatsapp.api_base_url}/#{business_account_id}/message_templates")
|
114
|
+
|
115
115
|
http = Net::HTTP.new(uri.host, uri.port)
|
116
116
|
http.use_ssl = true
|
117
117
|
|
@@ -124,8 +124,8 @@ module FlowChat
|
|
124
124
|
|
125
125
|
# Get template status
|
126
126
|
def template_status(template_id)
|
127
|
-
uri = URI("
|
128
|
-
|
127
|
+
uri = URI("#{FlowChat::Config.whatsapp.api_base_url}/#{template_id}")
|
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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow_chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -95,11 +95,13 @@ files:
|
|
95
95
|
- LICENSE.txt
|
96
96
|
- README.md
|
97
97
|
- Rakefile
|
98
|
+
- SECURITY.md
|
98
99
|
- bin/console
|
99
100
|
- bin/setup
|
100
101
|
- examples/initializer.rb
|
101
102
|
- examples/media_prompts_examples.rb
|
102
103
|
- examples/multi_tenant_whatsapp_controller.rb
|
104
|
+
- examples/simulator_controller.rb
|
103
105
|
- examples/ussd_controller.rb
|
104
106
|
- examples/whatsapp_controller.rb
|
105
107
|
- examples/whatsapp_media_examples.rb
|