flow_chat 0.6.0 → 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 +7 -7
- data/lib/flow_chat/ussd/app.rb +1 -1
- 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/app.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 +128 -54
- 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,93 +69,127 @@ 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
|
|
116
176
|
# Process the message based on handling mode
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
177
|
+
case handler_mode
|
178
|
+
when :inline
|
179
|
+
handle_message_inline(context, controller)
|
180
|
+
when :background
|
181
|
+
handle_message_background(context, controller)
|
182
|
+
when :simulator
|
183
|
+
# Return early from simulator mode to preserve the JSON response
|
184
|
+
return handle_message_simulator(context, controller)
|
137
185
|
end
|
138
186
|
end
|
139
187
|
|
140
188
|
# Handle message status updates
|
141
189
|
if value["statuses"]&.any?
|
142
|
-
|
190
|
+
statuses = value["statuses"]
|
191
|
+
FlowChat.logger.info { "CloudApi: Received #{statuses.size} status update(s)" }
|
192
|
+
FlowChat.logger.debug { "CloudApi: Status updates: #{statuses.inspect}" }
|
143
193
|
end
|
144
194
|
|
145
195
|
controller.head :ok
|
@@ -149,18 +199,23 @@ module FlowChat
|
|
149
199
|
def valid_webhook_signature?(request)
|
150
200
|
# Check if signature validation is explicitly disabled
|
151
201
|
if @config.skip_signature_validation
|
202
|
+
FlowChat.logger.debug { "CloudApi: Webhook signature validation is disabled" }
|
152
203
|
return true
|
153
204
|
end
|
154
205
|
|
155
206
|
# Require app_secret for signature validation
|
156
207
|
unless @config.app_secret && !@config.app_secret.empty?
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
160
212
|
end
|
161
213
|
|
162
214
|
signature_header = request.headers["X-Hub-Signature-256"]
|
163
|
-
|
215
|
+
unless signature_header
|
216
|
+
FlowChat.logger.warn { "CloudApi: No X-Hub-Signature-256 header found in request" }
|
217
|
+
return false
|
218
|
+
end
|
164
219
|
|
165
220
|
# Extract signature from header (format: "sha256=<signature>")
|
166
221
|
expected_signature = signature_header.sub("sha256=", "")
|
@@ -178,11 +233,19 @@ module FlowChat
|
|
178
233
|
)
|
179
234
|
|
180
235
|
# Compare signatures using secure comparison to prevent timing attacks
|
181
|
-
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
|
182
245
|
rescue FlowChat::Whatsapp::ConfigurationError
|
183
246
|
raise
|
184
247
|
rescue => e
|
185
|
-
|
248
|
+
FlowChat.logger.error { "CloudApi: Error validating webhook signature: #{e.class.name}: #{e.message}" }
|
186
249
|
false
|
187
250
|
end
|
188
251
|
|
@@ -197,24 +260,35 @@ module FlowChat
|
|
197
260
|
end
|
198
261
|
|
199
262
|
def extract_message_content(message, context)
|
200
|
-
|
263
|
+
message_type = message["type"]
|
264
|
+
FlowChat.logger.debug { "CloudApi: Extracting content from #{message_type} message" }
|
265
|
+
|
266
|
+
case message_type
|
201
267
|
when "text"
|
202
|
-
|
268
|
+
content = message.dig("text", "body")
|
269
|
+
context.input = content
|
270
|
+
FlowChat.logger.debug { "CloudApi: Text message content: '#{content}'" }
|
203
271
|
when "interactive"
|
204
272
|
# Handle button/list replies
|
205
273
|
if message.dig("interactive", "type") == "button_reply"
|
206
|
-
|
274
|
+
content = message.dig("interactive", "button_reply", "id")
|
275
|
+
context.input = content
|
276
|
+
FlowChat.logger.debug { "CloudApi: Button reply ID: '#{content}'" }
|
207
277
|
elsif message.dig("interactive", "type") == "list_reply"
|
208
|
-
|
278
|
+
content = message.dig("interactive", "list_reply", "id")
|
279
|
+
context.input = content
|
280
|
+
FlowChat.logger.debug { "CloudApi: List reply ID: '#{content}'" }
|
209
281
|
end
|
210
282
|
when "location"
|
211
|
-
|
283
|
+
location = {
|
212
284
|
latitude: message.dig("location", "latitude"),
|
213
285
|
longitude: message.dig("location", "longitude"),
|
214
286
|
name: message.dig("location", "name"),
|
215
287
|
address: message.dig("location", "address")
|
216
288
|
}
|
289
|
+
context["request.location"] = location
|
217
290
|
context.input = "$location$"
|
291
|
+
FlowChat.logger.debug { "CloudApi: Location received - Lat: #{location[:latitude]}, Lng: #{location[:longitude]}" }
|
218
292
|
when "image", "document", "audio", "video"
|
219
293
|
context["request.media"] = {
|
220
294
|
type: message["type"],
|
@@ -243,7 +317,7 @@ module FlowChat
|
|
243
317
|
if response
|
244
318
|
_type, prompt, choices, media = response
|
245
319
|
rendered_message = render_response(prompt, choices, media)
|
246
|
-
|
320
|
+
|
247
321
|
# Queue only the response delivery asynchronously
|
248
322
|
send_data = {
|
249
323
|
msisdn: context["request.msisdn"],
|
@@ -273,7 +347,7 @@ module FlowChat
|
|
273
347
|
if response
|
274
348
|
_type, prompt, choices, media = response
|
275
349
|
rendered_message = render_response(prompt, choices, media)
|
276
|
-
|
350
|
+
|
277
351
|
# For simulator mode, return the response data in the HTTP response
|
278
352
|
# instead of actually sending via WhatsApp API
|
279
353
|
message_payload = @client.build_message_payload(rendered_message, context["request.msisdn"])
|
@@ -297,7 +371,7 @@ module FlowChat
|
|
297
371
|
def simulate?(context)
|
298
372
|
# Check if simulator mode is enabled for this processor
|
299
373
|
return false unless context["enable_simulator"]
|
300
|
-
|
374
|
+
|
301
375
|
# Then check if simulator mode is requested and valid
|
302
376
|
@body.dig("simulator_mode") && valid_simulator_cookie?(context)
|
303
377
|
end
|
@@ -305,27 +379,27 @@ module FlowChat
|
|
305
379
|
def valid_simulator_cookie?(context)
|
306
380
|
simulator_secret = FlowChat::Config.simulator_secret
|
307
381
|
return false unless simulator_secret && !simulator_secret.empty?
|
308
|
-
|
382
|
+
|
309
383
|
# Check for simulator cookie
|
310
384
|
request = context.controller.request
|
311
385
|
simulator_cookie = request.cookies["flowchat_simulator"]
|
312
386
|
return false unless simulator_cookie
|
313
|
-
|
387
|
+
|
314
388
|
# Verify the cookie is a valid HMAC signature
|
315
389
|
# Cookie format: "timestamp:signature" where signature = HMAC(simulator_secret, "simulator:timestamp")
|
316
390
|
begin
|
317
391
|
timestamp_str, signature = simulator_cookie.split(":", 2)
|
318
392
|
return false unless timestamp_str && signature
|
319
|
-
|
393
|
+
|
320
394
|
# Check timestamp is recent (within 24 hours for reasonable session duration)
|
321
395
|
timestamp = timestamp_str.to_i
|
322
396
|
return false if timestamp <= 0
|
323
397
|
return false if (Time.now.to_i - timestamp).abs > 86400 # 24 hours
|
324
|
-
|
398
|
+
|
325
399
|
# Calculate expected signature
|
326
400
|
message = "simulator:#{timestamp_str}"
|
327
401
|
expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), simulator_secret, message)
|
328
|
-
|
402
|
+
|
329
403
|
# Use secure comparison
|
330
404
|
secure_compare(signature, expected_signature)
|
331
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: []
|