flow_chat 0.6.1 → 0.7.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/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +84 -1229
- data/docs/configuration.md +337 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +306 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +9 -18
- data/examples/ussd_controller.rb +32 -38
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +48 -2
- data/lib/flow_chat/config.rb +5 -0
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +37 -4
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +6 -6
- data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +14 -0
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +8 -0
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +22 -11
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
@@ -10,26 +10,39 @@ module FlowChat
|
|
10
10
|
|
11
11
|
module Gateway
|
12
12
|
class CloudApi
|
13
|
+
include FlowChat::Instrumentation
|
14
|
+
|
15
|
+
attr_reader :context
|
16
|
+
|
13
17
|
def initialize(app, config = nil)
|
14
18
|
@app = app
|
15
19
|
@config = config || FlowChat::Whatsapp::Configuration.from_credentials
|
16
20
|
@client = FlowChat::Whatsapp::Client.new(@config)
|
21
|
+
|
22
|
+
FlowChat.logger.info { "CloudApi: Initialized WhatsApp Cloud API gateway with phone_number_id: #{@config.phone_number_id}" }
|
23
|
+
FlowChat.logger.debug { "CloudApi: Gateway configuration - API base URL: #{FlowChat::Config.whatsapp.api_base_url}" }
|
17
24
|
end
|
18
25
|
|
19
26
|
def call(context)
|
27
|
+
@context = context
|
20
28
|
controller = context.controller
|
21
29
|
request = controller.request
|
22
30
|
|
31
|
+
FlowChat.logger.debug { "CloudApi: Processing #{request.request_method} request to #{request.path}" }
|
32
|
+
|
23
33
|
# Handle webhook verification
|
24
34
|
if request.get? && request.params["hub.mode"] == "subscribe"
|
35
|
+
FlowChat.logger.info { "CloudApi: Handling webhook verification request" }
|
25
36
|
return handle_verification(context)
|
26
37
|
end
|
27
38
|
|
28
39
|
# Handle webhook messages
|
29
40
|
if request.post?
|
41
|
+
FlowChat.logger.info { "CloudApi: Handling webhook message" }
|
30
42
|
return handle_webhook(context)
|
31
43
|
end
|
32
44
|
|
45
|
+
FlowChat.logger.warn { "CloudApi: Invalid request method or parameters - returning bad request" }
|
33
46
|
controller.head :bad_request
|
34
47
|
end
|
35
48
|
|
@@ -41,11 +54,14 @@ module FlowChat
|
|
41
54
|
def determine_message_handler(context)
|
42
55
|
# Check if simulator mode was already detected and set in context
|
43
56
|
if context["simulator_mode"]
|
57
|
+
FlowChat.logger.debug { "CloudApi: Using simulator message handler" }
|
44
58
|
return :simulator
|
45
59
|
end
|
46
60
|
|
47
61
|
# Use global WhatsApp configuration
|
48
|
-
FlowChat::Config.whatsapp.message_handling_mode
|
62
|
+
mode = FlowChat::Config.whatsapp.message_handling_mode
|
63
|
+
FlowChat.logger.debug { "CloudApi: Using #{mode} message handling mode" }
|
64
|
+
mode
|
49
65
|
end
|
50
66
|
|
51
67
|
def handle_verification(context)
|
@@ -53,63 +69,107 @@ module FlowChat
|
|
53
69
|
params = controller.request.params
|
54
70
|
|
55
71
|
verify_token = @config.verify_token
|
72
|
+
provided_token = params["hub.verify_token"]
|
73
|
+
challenge = params["hub.challenge"]
|
56
74
|
|
57
|
-
|
58
|
-
|
75
|
+
FlowChat.logger.debug { "CloudApi: Webhook verification - provided token matches: #{provided_token == verify_token}" }
|
76
|
+
|
77
|
+
if provided_token == verify_token
|
78
|
+
# Use instrumentation for webhook verification success
|
79
|
+
instrument(Events::WEBHOOK_VERIFIED, {
|
80
|
+
challenge: challenge,
|
81
|
+
platform: :whatsapp
|
82
|
+
})
|
83
|
+
|
84
|
+
controller.render plain: challenge
|
59
85
|
else
|
86
|
+
# Use instrumentation for webhook verification failure
|
87
|
+
instrument(Events::WEBHOOK_FAILED, {
|
88
|
+
reason: "Invalid verify token",
|
89
|
+
platform: :whatsapp
|
90
|
+
})
|
91
|
+
|
60
92
|
controller.head :forbidden
|
61
93
|
end
|
62
94
|
end
|
63
95
|
|
64
96
|
def handle_webhook(context)
|
65
97
|
controller = context.controller
|
66
|
-
|
98
|
+
|
67
99
|
# Parse body
|
68
100
|
begin
|
69
101
|
parse_request_body(controller.request)
|
102
|
+
FlowChat.logger.debug { "CloudApi: Successfully parsed webhook request body" }
|
70
103
|
rescue JSON::ParserError => e
|
71
|
-
|
104
|
+
FlowChat.logger.error { "CloudApi: Failed to parse webhook body: #{e.message}" }
|
72
105
|
return controller.head :bad_request
|
73
106
|
end
|
74
|
-
|
107
|
+
|
75
108
|
# Check for simulator mode parameter in request (before validation)
|
76
109
|
# But only enable if valid simulator token is provided
|
77
110
|
is_simulator_mode = simulate?(context)
|
78
111
|
if is_simulator_mode
|
112
|
+
FlowChat.logger.info { "CloudApi: Simulator mode enabled for this request" }
|
79
113
|
context["simulator_mode"] = true
|
80
114
|
end
|
81
115
|
|
82
116
|
# Validate webhook signature for security (skip for simulator mode)
|
83
117
|
unless is_simulator_mode || valid_webhook_signature?(controller.request)
|
84
|
-
|
118
|
+
FlowChat.logger.warn { "CloudApi: Invalid webhook signature received - rejecting request" }
|
85
119
|
return controller.head :unauthorized
|
86
120
|
end
|
87
121
|
|
122
|
+
FlowChat.logger.debug { "CloudApi: Webhook signature validation passed" }
|
123
|
+
|
88
124
|
# Extract message data from WhatsApp webhook
|
89
125
|
entry = @body.dig("entry", 0)
|
90
|
-
|
126
|
+
unless entry
|
127
|
+
FlowChat.logger.debug { "CloudApi: No entry found in webhook body - returning OK" }
|
128
|
+
return controller.head :ok
|
129
|
+
end
|
91
130
|
|
92
131
|
changes = entry.dig("changes", 0)
|
93
|
-
|
132
|
+
unless changes
|
133
|
+
FlowChat.logger.debug { "CloudApi: No changes found in webhook entry - returning OK" }
|
134
|
+
return controller.head :ok
|
135
|
+
end
|
94
136
|
|
95
137
|
value = changes["value"]
|
96
|
-
|
138
|
+
unless value
|
139
|
+
FlowChat.logger.debug { "CloudApi: No value found in webhook changes - returning OK" }
|
140
|
+
return controller.head :ok
|
141
|
+
end
|
97
142
|
|
98
143
|
# Handle incoming messages
|
99
144
|
if value["messages"]&.any?
|
100
145
|
message = value["messages"].first
|
101
146
|
contact = value["contacts"]&.first
|
102
147
|
|
103
|
-
|
148
|
+
phone_number = message["from"]
|
149
|
+
message_id = message["id"]
|
150
|
+
contact_name = contact&.dig("profile", "name")
|
151
|
+
|
152
|
+
# Use instrumentation for message received
|
153
|
+
instrument(Events::MESSAGE_RECEIVED, {
|
154
|
+
from: phone_number,
|
155
|
+
message: context.input,
|
156
|
+
message_type: message["type"],
|
157
|
+
message_id: message_id,
|
158
|
+
platform: :whatsapp
|
159
|
+
})
|
160
|
+
|
161
|
+
context["request.id"] = phone_number
|
104
162
|
context["request.gateway"] = :whatsapp_cloud_api
|
105
|
-
context["request.message_id"] =
|
106
|
-
context["request.msisdn"] = Phonelib.parse(
|
107
|
-
context["request.contact_name"] =
|
163
|
+
context["request.message_id"] = message_id
|
164
|
+
context["request.msisdn"] = Phonelib.parse(phone_number).e164
|
165
|
+
context["request.contact_name"] = contact_name
|
108
166
|
context["request.timestamp"] = message["timestamp"]
|
109
167
|
|
110
168
|
# Extract message content based on type
|
111
169
|
extract_message_content(message, context)
|
112
170
|
|
171
|
+
FlowChat.logger.debug { "CloudApi: Message content extracted - Type: #{message["type"]}, Input: '#{context.input}'" }
|
172
|
+
|
113
173
|
# Determine message handling mode
|
114
174
|
handler_mode = determine_message_handler(context)
|
115
175
|
|
@@ -127,7 +187,9 @@ module FlowChat
|
|
127
187
|
|
128
188
|
# Handle message status updates
|
129
189
|
if value["statuses"]&.any?
|
130
|
-
|
190
|
+
statuses = value["statuses"]
|
191
|
+
FlowChat.logger.info { "CloudApi: Received #{statuses.size} status update(s)" }
|
192
|
+
FlowChat.logger.debug { "CloudApi: Status updates: #{statuses.inspect}" }
|
131
193
|
end
|
132
194
|
|
133
195
|
controller.head :ok
|
@@ -137,18 +199,23 @@ module FlowChat
|
|
137
199
|
def valid_webhook_signature?(request)
|
138
200
|
# Check if signature validation is explicitly disabled
|
139
201
|
if @config.skip_signature_validation
|
202
|
+
FlowChat.logger.debug { "CloudApi: Webhook signature validation is disabled" }
|
140
203
|
return true
|
141
204
|
end
|
142
205
|
|
143
206
|
# Require app_secret for signature validation
|
144
207
|
unless @config.app_secret && !@config.app_secret.empty?
|
145
|
-
|
146
|
-
|
147
|
-
|
208
|
+
error_msg = "WhatsApp app_secret is required for webhook signature validation. " \
|
209
|
+
"Either configure app_secret or set skip_signature_validation=true to explicitly disable validation."
|
210
|
+
FlowChat.logger.error { "CloudApi: #{error_msg}" }
|
211
|
+
raise FlowChat::Whatsapp::ConfigurationError, error_msg
|
148
212
|
end
|
149
213
|
|
150
214
|
signature_header = request.headers["X-Hub-Signature-256"]
|
151
|
-
|
215
|
+
unless signature_header
|
216
|
+
FlowChat.logger.warn { "CloudApi: No X-Hub-Signature-256 header found in request" }
|
217
|
+
return false
|
218
|
+
end
|
152
219
|
|
153
220
|
# Extract signature from header (format: "sha256=<signature>")
|
154
221
|
expected_signature = signature_header.sub("sha256=", "")
|
@@ -166,11 +233,19 @@ module FlowChat
|
|
166
233
|
)
|
167
234
|
|
168
235
|
# Compare signatures using secure comparison to prevent timing attacks
|
169
|
-
secure_compare(expected_signature, calculated_signature)
|
236
|
+
signature_valid = secure_compare(expected_signature, calculated_signature)
|
237
|
+
|
238
|
+
if signature_valid
|
239
|
+
FlowChat.logger.debug { "CloudApi: Webhook signature validation successful" }
|
240
|
+
else
|
241
|
+
FlowChat.logger.warn { "CloudApi: Webhook signature validation failed - signatures do not match" }
|
242
|
+
end
|
243
|
+
|
244
|
+
signature_valid
|
170
245
|
rescue FlowChat::Whatsapp::ConfigurationError
|
171
246
|
raise
|
172
247
|
rescue => e
|
173
|
-
|
248
|
+
FlowChat.logger.error { "CloudApi: Error validating webhook signature: #{e.class.name}: #{e.message}" }
|
174
249
|
false
|
175
250
|
end
|
176
251
|
|
@@ -185,24 +260,35 @@ module FlowChat
|
|
185
260
|
end
|
186
261
|
|
187
262
|
def extract_message_content(message, context)
|
188
|
-
|
263
|
+
message_type = message["type"]
|
264
|
+
FlowChat.logger.debug { "CloudApi: Extracting content from #{message_type} message" }
|
265
|
+
|
266
|
+
case message_type
|
189
267
|
when "text"
|
190
|
-
|
268
|
+
content = message.dig("text", "body")
|
269
|
+
context.input = content
|
270
|
+
FlowChat.logger.debug { "CloudApi: Text message content: '#{content}'" }
|
191
271
|
when "interactive"
|
192
272
|
# Handle button/list replies
|
193
273
|
if message.dig("interactive", "type") == "button_reply"
|
194
|
-
|
274
|
+
content = message.dig("interactive", "button_reply", "id")
|
275
|
+
context.input = content
|
276
|
+
FlowChat.logger.debug { "CloudApi: Button reply ID: '#{content}'" }
|
195
277
|
elsif message.dig("interactive", "type") == "list_reply"
|
196
|
-
|
278
|
+
content = message.dig("interactive", "list_reply", "id")
|
279
|
+
context.input = content
|
280
|
+
FlowChat.logger.debug { "CloudApi: List reply ID: '#{content}'" }
|
197
281
|
end
|
198
282
|
when "location"
|
199
|
-
|
283
|
+
location = {
|
200
284
|
latitude: message.dig("location", "latitude"),
|
201
285
|
longitude: message.dig("location", "longitude"),
|
202
286
|
name: message.dig("location", "name"),
|
203
287
|
address: message.dig("location", "address")
|
204
288
|
}
|
289
|
+
context["request.location"] = location
|
205
290
|
context.input = "$location$"
|
291
|
+
FlowChat.logger.debug { "CloudApi: Location received - Lat: #{location[:latitude]}, Lng: #{location[:longitude]}" }
|
206
292
|
when "image", "document", "audio", "video"
|
207
293
|
context["request.media"] = {
|
208
294
|
type: message["type"],
|
@@ -231,7 +317,7 @@ module FlowChat
|
|
231
317
|
if response
|
232
318
|
_type, prompt, choices, media = response
|
233
319
|
rendered_message = render_response(prompt, choices, media)
|
234
|
-
|
320
|
+
|
235
321
|
# Queue only the response delivery asynchronously
|
236
322
|
send_data = {
|
237
323
|
msisdn: context["request.msisdn"],
|
@@ -261,7 +347,7 @@ module FlowChat
|
|
261
347
|
if response
|
262
348
|
_type, prompt, choices, media = response
|
263
349
|
rendered_message = render_response(prompt, choices, media)
|
264
|
-
|
350
|
+
|
265
351
|
# For simulator mode, return the response data in the HTTP response
|
266
352
|
# instead of actually sending via WhatsApp API
|
267
353
|
message_payload = @client.build_message_payload(rendered_message, context["request.msisdn"])
|
@@ -285,7 +371,7 @@ module FlowChat
|
|
285
371
|
def simulate?(context)
|
286
372
|
# Check if simulator mode is enabled for this processor
|
287
373
|
return false unless context["enable_simulator"]
|
288
|
-
|
374
|
+
|
289
375
|
# Then check if simulator mode is requested and valid
|
290
376
|
@body.dig("simulator_mode") && valid_simulator_cookie?(context)
|
291
377
|
end
|
@@ -293,27 +379,27 @@ module FlowChat
|
|
293
379
|
def valid_simulator_cookie?(context)
|
294
380
|
simulator_secret = FlowChat::Config.simulator_secret
|
295
381
|
return false unless simulator_secret && !simulator_secret.empty?
|
296
|
-
|
382
|
+
|
297
383
|
# Check for simulator cookie
|
298
384
|
request = context.controller.request
|
299
385
|
simulator_cookie = request.cookies["flowchat_simulator"]
|
300
386
|
return false unless simulator_cookie
|
301
|
-
|
387
|
+
|
302
388
|
# Verify the cookie is a valid HMAC signature
|
303
389
|
# Cookie format: "timestamp:signature" where signature = HMAC(simulator_secret, "simulator:timestamp")
|
304
390
|
begin
|
305
391
|
timestamp_str, signature = simulator_cookie.split(":", 2)
|
306
392
|
return false unless timestamp_str && signature
|
307
|
-
|
393
|
+
|
308
394
|
# Check timestamp is recent (within 24 hours for reasonable session duration)
|
309
395
|
timestamp = timestamp_str.to_i
|
310
396
|
return false if timestamp <= 0
|
311
397
|
return false if (Time.now.to_i - timestamp).abs > 86400 # 24 hours
|
312
|
-
|
398
|
+
|
313
399
|
# Calculate expected signature
|
314
400
|
message = "simulator:#{timestamp_str}"
|
315
401
|
expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), simulator_secret, message)
|
316
|
-
|
402
|
+
|
317
403
|
# Use secure comparison
|
318
404
|
secure_compare(signature, expected_signature)
|
319
405
|
rescue => e
|
@@ -4,24 +4,46 @@ module FlowChat
|
|
4
4
|
class Executor
|
5
5
|
def initialize(app)
|
6
6
|
@app = app
|
7
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Initialized WhatsApp executor middleware" }
|
7
8
|
end
|
8
9
|
|
9
10
|
def call(context)
|
11
|
+
flow_class = context.flow
|
12
|
+
action = context["flow.action"]
|
13
|
+
session_id = context["session.id"]
|
14
|
+
|
15
|
+
FlowChat.logger.info { "Whatsapp::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
|
16
|
+
|
10
17
|
whatsapp_app = build_whatsapp_app context
|
11
|
-
|
12
|
-
|
18
|
+
FlowChat.logger.debug { "Whatsapp::Executor: WhatsApp app built for flow execution" }
|
19
|
+
|
20
|
+
flow = flow_class.new whatsapp_app
|
21
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Flow instance created, invoking #{action} method" }
|
22
|
+
|
23
|
+
flow.send action
|
24
|
+
FlowChat.logger.warn { "Whatsapp::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
|
25
|
+
raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
|
13
26
|
rescue FlowChat::Interrupt::Prompt => e
|
27
|
+
FlowChat.logger.info { "Whatsapp::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
|
28
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
|
14
29
|
# Return the same triplet format as USSD for consistency
|
15
30
|
[:prompt, e.prompt, e.choices, e.media]
|
16
31
|
rescue FlowChat::Interrupt::Terminate => e
|
32
|
+
FlowChat.logger.info { "Whatsapp::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
|
33
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Destroying session #{session_id}" }
|
17
34
|
# Clean up session and return terminal message
|
18
35
|
context.session.destroy
|
19
36
|
[:terminate, e.prompt, nil, e.media]
|
37
|
+
rescue => error
|
38
|
+
FlowChat.logger.error { "Whatsapp::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
|
39
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Stack trace: #{error.backtrace.join("\n")}" }
|
40
|
+
raise
|
20
41
|
end
|
21
42
|
|
22
43
|
private
|
23
44
|
|
24
45
|
def build_whatsapp_app(context)
|
46
|
+
FlowChat.logger.debug { "Whatsapp::Executor: Building WhatsApp app instance" }
|
25
47
|
FlowChat::Whatsapp::App.new(context)
|
26
48
|
end
|
27
49
|
end
|
@@ -2,6 +2,7 @@ module FlowChat
|
|
2
2
|
module Whatsapp
|
3
3
|
class Processor < FlowChat::BaseProcessor
|
4
4
|
def use_whatsapp_config(config)
|
5
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp config: #{config.class.name}" }
|
5
6
|
@whatsapp_config = config
|
6
7
|
self
|
7
8
|
end
|
@@ -13,13 +14,20 @@ module FlowChat
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def build_middleware_stack
|
17
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Building WhatsApp middleware stack" }
|
16
18
|
create_middleware_stack("whatsapp")
|
17
19
|
end
|
18
20
|
|
19
21
|
def configure_middleware_stack(builder)
|
22
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp middleware stack" }
|
20
23
|
builder.use FlowChat::Session::Middleware
|
24
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Added Session::Middleware" }
|
25
|
+
|
21
26
|
builder.use middleware
|
27
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Added custom middleware" }
|
28
|
+
|
22
29
|
builder.use FlowChat::Whatsapp::Middleware::Executor
|
30
|
+
FlowChat.logger.debug { "Whatsapp::Processor: Added Whatsapp::Middleware::Executor" }
|
23
31
|
end
|
24
32
|
end
|
25
33
|
end
|
@@ -49,15 +49,10 @@ module FlowChat
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def build_selection_message
|
52
|
-
|
53
|
-
if choices.is_a?(Array)
|
54
|
-
# Convert array to hash with index-based keys
|
55
|
-
choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
|
56
|
-
build_interactive_message(choice_hash)
|
57
|
-
elsif choices.is_a?(Hash)
|
52
|
+
if choices.is_a?(Hash)
|
58
53
|
build_interactive_message(choices)
|
59
54
|
else
|
60
|
-
raise ArgumentError, "choices must be
|
55
|
+
raise ArgumentError, "choices must be a Hash"
|
61
56
|
end
|
62
57
|
end
|
63
58
|
|
@@ -121,7 +116,7 @@ module FlowChat
|
|
121
116
|
}
|
122
117
|
when :video
|
123
118
|
{
|
124
|
-
type: "video",
|
119
|
+
type: "video",
|
125
120
|
video: {link: url}
|
126
121
|
}
|
127
122
|
when :document
|
@@ -188,4 +183,4 @@ module FlowChat
|
|
188
183
|
end
|
189
184
|
end
|
190
185
|
end
|
191
|
-
end
|
186
|
+
end
|
data/lib/flow_chat.rb
CHANGED
@@ -2,6 +2,8 @@ require "zeitwerk"
|
|
2
2
|
require "active_support"
|
3
3
|
require "active_support/core_ext/time"
|
4
4
|
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/string/filters"
|
6
|
+
require "active_support/core_ext/enumerable"
|
5
7
|
|
6
8
|
loader = Zeitwerk::Loader.for_gem
|
7
9
|
loader.enable_reloading if defined?(Rails.env) && Rails.env.development?
|
@@ -11,6 +13,27 @@ module FlowChat
|
|
11
13
|
def self.root
|
12
14
|
Pathname.new __dir__
|
13
15
|
end
|
16
|
+
|
17
|
+
def self.setup_instrumentation!
|
18
|
+
require_relative "flow_chat/instrumentation/setup"
|
19
|
+
FlowChat::Instrumentation::Setup.setup_instrumentation!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Access to instrumentation
|
23
|
+
def self.instrument(event_name, payload = {}, &block)
|
24
|
+
FlowChat::Instrumentation.instrument(event_name, payload, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.metrics
|
28
|
+
FlowChat::Instrumentation::Setup.metrics_collector
|
29
|
+
end
|
14
30
|
end
|
15
31
|
|
16
32
|
loader.eager_load
|
33
|
+
|
34
|
+
# Auto-setup instrumentation in Rails environments
|
35
|
+
if defined?(Rails)
|
36
|
+
Rails.application.config.after_initialize do
|
37
|
+
FlowChat.setup_instrumentation!
|
38
|
+
end
|
39
|
+
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.7.0
|
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-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -80,7 +80,11 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.4.2
|
83
|
-
description:
|
83
|
+
description: "FlowChat is a Rails framework for building sophisticated conversational
|
84
|
+
interfaces across USSD and WhatsApp platforms. \nCreate interactive flows with menus,
|
85
|
+
prompts, validation, media support, and session management. Features include \nmulti-tenancy,
|
86
|
+
background job processing, built-in simulator for testing, and comprehensive middleware
|
87
|
+
support.\n"
|
84
88
|
email:
|
85
89
|
- sfroelich01@gmail.com
|
86
90
|
executables: []
|
@@ -88,21 +92,24 @@ extensions: []
|
|
88
92
|
extra_rdoc_files: []
|
89
93
|
files:
|
90
94
|
- ".DS_Store"
|
95
|
+
- ".github/workflows/ci.yml"
|
91
96
|
- ".gitignore"
|
92
97
|
- ".ruby-version"
|
93
|
-
- ".travis.yml"
|
94
98
|
- Gemfile
|
95
99
|
- LICENSE.txt
|
96
100
|
- README.md
|
97
101
|
- Rakefile
|
98
102
|
- SECURITY.md
|
99
|
-
- app/controllers/demo_controller.rb
|
100
|
-
- app/flow_chat/demo_restaurant_flow.rb
|
101
103
|
- bin/console
|
102
104
|
- bin/setup
|
103
|
-
-
|
104
|
-
-
|
105
|
-
-
|
105
|
+
- docs/configuration.md
|
106
|
+
- docs/flows.md
|
107
|
+
- docs/images/simulator.png
|
108
|
+
- docs/instrumentation.md
|
109
|
+
- docs/media.md
|
110
|
+
- docs/testing.md
|
111
|
+
- docs/ussd-setup.md
|
112
|
+
- docs/whatsapp-setup.md
|
106
113
|
- examples/multi_tenant_whatsapp_controller.rb
|
107
114
|
- examples/simulator_controller.rb
|
108
115
|
- examples/ussd_controller.rb
|
@@ -110,12 +117,15 @@ files:
|
|
110
117
|
- examples/whatsapp_media_examples.rb
|
111
118
|
- examples/whatsapp_message_job.rb
|
112
119
|
- flow_chat.gemspec
|
113
|
-
- images/ussd_simulator.png
|
114
120
|
- lib/flow_chat.rb
|
115
121
|
- lib/flow_chat/base_processor.rb
|
116
122
|
- lib/flow_chat/config.rb
|
117
123
|
- lib/flow_chat/context.rb
|
118
124
|
- lib/flow_chat/flow.rb
|
125
|
+
- lib/flow_chat/instrumentation.rb
|
126
|
+
- lib/flow_chat/instrumentation/log_subscriber.rb
|
127
|
+
- lib/flow_chat/instrumentation/metrics_collector.rb
|
128
|
+
- lib/flow_chat/instrumentation/setup.rb
|
119
129
|
- lib/flow_chat/interrupt.rb
|
120
130
|
- lib/flow_chat/prompt.rb
|
121
131
|
- lib/flow_chat/session/cache_session_store.rb
|
@@ -126,6 +136,7 @@ files:
|
|
126
136
|
- lib/flow_chat/ussd/app.rb
|
127
137
|
- lib/flow_chat/ussd/gateway/nalo.rb
|
128
138
|
- lib/flow_chat/ussd/gateway/nsano.rb
|
139
|
+
- lib/flow_chat/ussd/middleware/choice_mapper.rb
|
129
140
|
- lib/flow_chat/ussd/middleware/executor.rb
|
130
141
|
- lib/flow_chat/ussd/middleware/pagination.rb
|
131
142
|
- lib/flow_chat/ussd/middleware/resumable_session.rb
|
@@ -167,5 +178,5 @@ requirements: []
|
|
167
178
|
rubygems_version: 3.4.10
|
168
179
|
signing_key:
|
169
180
|
specification_version: 4
|
170
|
-
summary:
|
181
|
+
summary: Build conversational interfaces for USSD and WhatsApp with Rails
|
171
182
|
test_files: []
|