flow_chat 0.7.0 → 0.8.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/README.md +1 -0
- data/docs/configuration.md +29 -6
- data/docs/sessions.md +433 -0
- data/docs/testing.md +2 -2
- data/docs/ussd-setup.md +20 -4
- data/examples/simulator_controller.rb +4 -4
- data/examples/ussd_controller.rb +9 -3
- data/lib/flow_chat/base_processor.rb +33 -2
- data/lib/flow_chat/config.rb +26 -5
- data/lib/flow_chat/session/middleware.rb +102 -17
- data/lib/flow_chat/simulator/controller.rb +2 -2
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/gateway/nalo.rb +1 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +3 -2
- data/lib/flow_chat/ussd/processor.rb +5 -7
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +1 -0
- data/lib/flow_chat/whatsapp/processor.rb +0 -2
- metadata +3 -3
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ebeb5cdcbacce73ec580e381554dbd2377f7896d7d06c488970051f6a8bf599
|
4
|
+
data.tar.gz: bc33f9f54c78264f416e00163e2ba0cf78da2e8e0961c942bb3aab1e86e783e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34006bb23ddaf1aefd8ec4876cb70d8f06d2bbb23da48663ff7a40dd630d9b01af58d9980c343c9d933fa637512375b5c7ba3b0b336ba91a314fdd9ae5fb2ba1
|
7
|
+
data.tar.gz: 78a242f2b175f57ce29326f342c9d4bf4c7c4402c0e2fcd634c21322280dcd0ac273d86eb19750702472218e9d08fd97267ad711b3673f108f89617e5319b9f7
|
data/README.md
CHANGED
@@ -186,6 +186,7 @@ See the [Testing Guide](docs/testing.md) for complete setup instructions and tes
|
|
186
186
|
- **[WhatsApp Setup](docs/whatsapp-setup.md)** - Comprehensive WhatsApp configuration
|
187
187
|
- **[USSD Setup](docs/ussd-setup.md)** - USSD gateway configuration and examples
|
188
188
|
- **[Flow Development](docs/flows.md)** - Advanced flow patterns and techniques
|
189
|
+
- **[Session Management](docs/sessions.md)** - Session architecture and configuration
|
189
190
|
- **[Media Support](docs/media.md)** - Rich media handling for WhatsApp
|
190
191
|
- **[Testing Guide](docs/testing.md)** - Complete testing strategies
|
191
192
|
- **[Configuration Reference](docs/configuration.md)** - All configuration options
|
data/docs/configuration.md
CHANGED
@@ -19,6 +19,26 @@ FlowChat::Config.combine_validation_error_with_message = true # default
|
|
19
19
|
FlowChat.setup_instrumentation!
|
20
20
|
```
|
21
21
|
|
22
|
+
## Session Configuration
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
# Session boundaries control how session IDs are constructed
|
26
|
+
FlowChat::Config.session.boundaries = [:flow, :platform] # default
|
27
|
+
FlowChat::Config.session.hash_phone_numbers = true # hash phone numbers for privacy
|
28
|
+
FlowChat::Config.session.identifier = nil # let platforms choose (default)
|
29
|
+
|
30
|
+
# Available boundary options:
|
31
|
+
# :flow - separate sessions per flow class
|
32
|
+
# :platform - separate sessions per platform (ussd, whatsapp)
|
33
|
+
# :gateway - separate sessions per gateway
|
34
|
+
# [] - global sessions (no boundaries)
|
35
|
+
|
36
|
+
# Available identifier options:
|
37
|
+
# nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
|
38
|
+
# :msisdn - use phone number (durable sessions)
|
39
|
+
# :request_id - use request ID (ephemeral sessions)
|
40
|
+
```
|
41
|
+
|
22
42
|
## USSD Configuration
|
23
43
|
|
24
44
|
```ruby
|
@@ -28,10 +48,6 @@ FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next
|
|
28
48
|
FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
|
29
49
|
FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
|
30
50
|
FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
|
31
|
-
|
32
|
-
# Resumable sessions
|
33
|
-
FlowChat::Config.ussd.resumable_sessions_enabled = true # default
|
34
|
-
FlowChat::Config.ussd.resumable_sessions_timeout_seconds = 300 # 5 minutes
|
35
51
|
```
|
36
52
|
|
37
53
|
## WhatsApp Configuration
|
@@ -174,8 +190,15 @@ processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
174
190
|
# Optional middleware
|
175
191
|
config.use_middleware MyCustomMiddleware
|
176
192
|
|
177
|
-
#
|
178
|
-
config.
|
193
|
+
# Configure session boundaries
|
194
|
+
config.use_session_config(
|
195
|
+
boundaries: [:flow, :platform], # which boundaries to enforce
|
196
|
+
hash_phone_numbers: true, # hash phone numbers for privacy
|
197
|
+
identifier: :msisdn # use MSISDN for durable sessions (optional)
|
198
|
+
)
|
199
|
+
|
200
|
+
# Shorthand for durable sessions (identifier: :msisdn)
|
201
|
+
config.use_durable_sessions
|
179
202
|
end
|
180
203
|
```
|
181
204
|
|
data/docs/sessions.md
ADDED
@@ -0,0 +1,433 @@
|
|
1
|
+
# Session Management
|
2
|
+
|
3
|
+
FlowChat provides a powerful and flexible session management system that enables persistent conversational state across multiple requests. This document covers the architecture, configuration, and best practices for session management.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Sessions in FlowChat store user conversation state between requests, enabling:
|
8
|
+
|
9
|
+
- **Multi-step workflows** - Collect information across multiple prompts
|
10
|
+
- **Context preservation** - Remember user inputs and conversation history
|
11
|
+
- **Cross-platform consistency** - Same session behavior across USSD and WhatsApp
|
12
|
+
- **Privacy protection** - Automatic phone number hashing for security
|
13
|
+
- **Flexible isolation** - Control session boundaries per deployment needs
|
14
|
+
|
15
|
+
## Architecture
|
16
|
+
|
17
|
+
The session system is built around three core components:
|
18
|
+
|
19
|
+
### 1. Session Configuration (`FlowChat::Config::SessionConfig`)
|
20
|
+
|
21
|
+
Controls how session IDs are generated and sessions are isolated:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# Global session configuration
|
25
|
+
FlowChat::Config.session.boundaries = [:flow, :gateway, :platform] # isolation boundaries
|
26
|
+
FlowChat::Config.session.hash_phone_numbers = true # privacy protection
|
27
|
+
FlowChat::Config.session.identifier = nil # platform chooses default
|
28
|
+
```
|
29
|
+
|
30
|
+
### 2. Session Middleware (`FlowChat::Session::Middleware`)
|
31
|
+
|
32
|
+
Automatically manages session creation and ID generation based on configuration:
|
33
|
+
|
34
|
+
- Generates consistent session IDs based on boundaries and identifiers
|
35
|
+
- Creates session store instances for each request
|
36
|
+
- Handles platform-specific defaults (USSD = ephemeral, WhatsApp = durable)
|
37
|
+
- Provides instrumentation events for monitoring
|
38
|
+
|
39
|
+
### 3. Session Stores
|
40
|
+
|
41
|
+
Store actual session data with different persistence strategies:
|
42
|
+
|
43
|
+
- **`FlowChat::Session::CacheSessionStore`** - Uses Rails cache (recommended)
|
44
|
+
- **`FlowChat::Session::RailsSessionStore`** - Uses Rails session (limited)
|
45
|
+
|
46
|
+
## Session Boundaries
|
47
|
+
|
48
|
+
Boundaries control how session IDs are constructed, determining when sessions are shared vs. isolated:
|
49
|
+
|
50
|
+
### Available Boundaries
|
51
|
+
|
52
|
+
- **`:flow`** - Separate sessions per flow class
|
53
|
+
- **`:platform`** - Separate sessions per platform (ussd, whatsapp)
|
54
|
+
- **`:gateway`** - Separate sessions per gateway
|
55
|
+
- **`:url`** - Separate sessions per request URL (host + path)
|
56
|
+
- **`[]`** - Global sessions (no boundaries)
|
57
|
+
|
58
|
+
### Examples
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Default: Full isolation
|
62
|
+
FlowChat::Config.session.boundaries = [:flow, :gateway, :platform]
|
63
|
+
# Session ID: "registration_flow:nalo:ussd:abc123"
|
64
|
+
|
65
|
+
# Flow isolation only
|
66
|
+
FlowChat::Config.session.boundaries = [:flow]
|
67
|
+
# Session ID: "registration_flow:abc123"
|
68
|
+
|
69
|
+
# Platform isolation only
|
70
|
+
FlowChat::Config.session.boundaries = [:platform]
|
71
|
+
# Session ID: "ussd:abc123"
|
72
|
+
|
73
|
+
# Global sessions
|
74
|
+
FlowChat::Config.session.boundaries = []
|
75
|
+
# Session ID: "abc123"
|
76
|
+
```
|
77
|
+
|
78
|
+
## Session Identifiers
|
79
|
+
|
80
|
+
Identifiers determine what makes a session unique:
|
81
|
+
|
82
|
+
### Available Identifiers
|
83
|
+
|
84
|
+
- **`nil`** - Platform chooses default (recommended)
|
85
|
+
- USSD: `:request_id` (ephemeral sessions)
|
86
|
+
- WhatsApp: `:msisdn` (durable sessions)
|
87
|
+
- **`:msisdn`** - Use phone number (durable sessions)
|
88
|
+
- **`:request_id`** - Use request ID (ephemeral sessions)
|
89
|
+
|
90
|
+
### Platform Defaults
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# USSD: ephemeral sessions by default
|
94
|
+
identifier: :request_id
|
95
|
+
# New session each time USSD times out
|
96
|
+
|
97
|
+
# WhatsApp: durable sessions by default
|
98
|
+
identifier: :msisdn
|
99
|
+
# Same session resumes across conversations
|
100
|
+
```
|
101
|
+
|
102
|
+
### Phone Number Hashing
|
103
|
+
|
104
|
+
When using `:msisdn` identifier, phone numbers are automatically hashed for privacy:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
FlowChat::Config.session.hash_phone_numbers = true # default
|
108
|
+
# "+256700123456" becomes "a1b2c3d4" (8-character hash)
|
109
|
+
|
110
|
+
FlowChat::Config.session.hash_phone_numbers = false
|
111
|
+
# "+256700123456" used directly (not recommended for production)
|
112
|
+
```
|
113
|
+
|
114
|
+
## Configuration Examples
|
115
|
+
|
116
|
+
### Basic USSD Configuration
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
120
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
121
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
122
|
+
|
123
|
+
# Use shorthand for standard durable sessions
|
124
|
+
config.use_durable_sessions
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### Custom Session Configuration
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
132
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
133
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
134
|
+
|
135
|
+
# Explicit session configuration
|
136
|
+
config.use_session_config(
|
137
|
+
boundaries: [:flow, :platform], # isolate by flow and platform
|
138
|
+
hash_phone_numbers: true, # hash phone numbers for privacy
|
139
|
+
identifier: :msisdn # use phone number for durable sessions
|
140
|
+
)
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Cross-gateway Sessions
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
148
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
149
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
150
|
+
|
151
|
+
# Allow sessions to work across different gateways
|
152
|
+
config.use_session_config(boundaries: [:flow, :platform])
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Cross-Platform Sessions
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
160
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
161
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
162
|
+
|
163
|
+
# Allow same user to continue on USSD or WhatsApp
|
164
|
+
config.use_cross_platform_sessions
|
165
|
+
# Equivalent to: boundaries: [:flow]
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### URL-Based Session Isolation
|
170
|
+
|
171
|
+
Perfect for multi-tenant applications:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
175
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
176
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
177
|
+
|
178
|
+
# Isolate sessions by URL (great for multi-tenant SaaS)
|
179
|
+
config.use_url_isolation
|
180
|
+
# Adds :url to existing boundaries
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
**URL Boundary Examples:**
|
185
|
+
- `tenant1.example.com/ussd` vs `tenant2.example.com/ussd` - Different sessions
|
186
|
+
- `api.example.com/v1/ussd` vs `api.example.com/v2/ussd` - Different sessions
|
187
|
+
- `dev.example.com/ussd` vs `prod.example.com/ussd` - Different sessions
|
188
|
+
|
189
|
+
**URL Processing:**
|
190
|
+
- Combines host + path: `example.com/api/v1/ussd`
|
191
|
+
- Sanitizes special characters: `tenant-1.com/ussd` → `tenant_1.com/ussd`
|
192
|
+
- Hashes long URLs (>50 chars): `verylongdomain.../path` → `url_a1b2c3d4`
|
193
|
+
|
194
|
+
### Global Sessions
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
198
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
199
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
200
|
+
|
201
|
+
# Single session shared across everything
|
202
|
+
config.use_session_config(boundaries: [])
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
## Session Stores
|
207
|
+
|
208
|
+
### Cache Session Store (Recommended)
|
209
|
+
|
210
|
+
Uses Rails cache backend with automatic TTL management:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
214
|
+
|
215
|
+
# Requires Rails cache to be configured
|
216
|
+
# config/environments/production.rb
|
217
|
+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
218
|
+
```
|
219
|
+
|
220
|
+
**Features:**
|
221
|
+
- Automatic session expiration via cache TTL
|
222
|
+
- Redis/Memcached support for distributed deployments
|
223
|
+
- High performance
|
224
|
+
- Memory efficient
|
225
|
+
|
226
|
+
### Rails Session Store
|
227
|
+
|
228
|
+
Uses Rails session for storage (limited scope):
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
232
|
+
```
|
233
|
+
|
234
|
+
**Limitations:**
|
235
|
+
- Tied to HTTP session lifecycle
|
236
|
+
- Limited storage capacity
|
237
|
+
- Not suitable for long-running conversations
|
238
|
+
|
239
|
+
## Session Data Usage
|
240
|
+
|
241
|
+
### In Flows
|
242
|
+
|
243
|
+
Sessions are automatically available in flows:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class RegistrationFlow < FlowChat::Flow
|
247
|
+
def main_page
|
248
|
+
# Store data
|
249
|
+
app.session.set("step", "registration")
|
250
|
+
app.session.set("user_data", {name: "John", age: 25})
|
251
|
+
|
252
|
+
# Retrieve data
|
253
|
+
step = app.session.get("step")
|
254
|
+
user_data = app.session.get("user_data")
|
255
|
+
|
256
|
+
# Check existence
|
257
|
+
if app.session.get("completed")
|
258
|
+
app.say "Registration already completed!"
|
259
|
+
return
|
260
|
+
end
|
261
|
+
|
262
|
+
# Continue flow...
|
263
|
+
name = app.screen(:name) { |prompt| prompt.ask "Name?" }
|
264
|
+
app.session.set("name", name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
### Session Store API
|
270
|
+
|
271
|
+
All session stores implement a consistent interface:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# Basic operations
|
275
|
+
session.set(key, value) # Store data
|
276
|
+
value = session.get(key) # Retrieve data
|
277
|
+
session.delete(key) # Delete specific key
|
278
|
+
session.clear # Clear all session data
|
279
|
+
session.destroy # Destroy entire session
|
280
|
+
|
281
|
+
# Utility methods
|
282
|
+
session.exists? # Check if session has any data
|
283
|
+
```
|
284
|
+
|
285
|
+
## Best Practices
|
286
|
+
|
287
|
+
### 1. Choose Appropriate Boundaries
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# High-traffic public services
|
291
|
+
boundaries: [:flow, :gateway, :platform] # Full isolation
|
292
|
+
|
293
|
+
# Single-tenant applications
|
294
|
+
boundaries: [:flow] # Simpler, allows cross-platform
|
295
|
+
|
296
|
+
# Global state services (rare)
|
297
|
+
boundaries: [] # Shared state across everything
|
298
|
+
```
|
299
|
+
|
300
|
+
### 2. Consider Session Lifecycle
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
# USSD: Short sessions, frequent timeouts
|
304
|
+
identifier: :request_id # New session each timeout (default)
|
305
|
+
|
306
|
+
# WhatsApp: Long conversations, persistent
|
307
|
+
identifier: :msisdn # Resume across days/weeks (default)
|
308
|
+
|
309
|
+
# Custom requirements
|
310
|
+
identifier: :msisdn # Make USSD durable
|
311
|
+
identifier: :request_id # Make WhatsApp ephemeral
|
312
|
+
```
|
313
|
+
|
314
|
+
### 3. Handle Session Expiration
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
class RegistrationFlow < FlowChat::Flow
|
318
|
+
def main_page
|
319
|
+
# Check if session expired
|
320
|
+
if app.session.get("user_id").nil?
|
321
|
+
restart_registration
|
322
|
+
return
|
323
|
+
end
|
324
|
+
|
325
|
+
# Continue existing session
|
326
|
+
continue_registration
|
327
|
+
end
|
328
|
+
|
329
|
+
private
|
330
|
+
|
331
|
+
def restart_registration
|
332
|
+
app.say "Session expired. Let's start over."
|
333
|
+
# Reset flow to beginning
|
334
|
+
end
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
### 4. Optimize Session Data
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
# Store only necessary data
|
342
|
+
app.session.set("user_id", 123) # Good: minimal data
|
343
|
+
app.session.set("user", user_object) # Avoid: large objects
|
344
|
+
|
345
|
+
# Clean up when done
|
346
|
+
def complete_registration
|
347
|
+
app.session.set("completed", true)
|
348
|
+
app.session.delete("temp_data") # Clean up temporary data
|
349
|
+
end
|
350
|
+
```
|
351
|
+
|
352
|
+
### 5. Security Considerations
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
# Always hash phone numbers in production
|
356
|
+
FlowChat::Config.session.hash_phone_numbers = true
|
357
|
+
|
358
|
+
# Use secure cache backends
|
359
|
+
config.cache_store = :redis_cache_store, {
|
360
|
+
url: ENV['REDIS_URL'],
|
361
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
362
|
+
}
|
363
|
+
|
364
|
+
# Set appropriate TTLs
|
365
|
+
cache_options = { expires_in: 30.minutes } # Reasonable session timeout
|
366
|
+
```
|
367
|
+
|
368
|
+
## Troubleshooting
|
369
|
+
|
370
|
+
### Session Not Persisting
|
371
|
+
|
372
|
+
1. **Check session store configuration:**
|
373
|
+
```ruby
|
374
|
+
# Ensure session store is configured
|
375
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
376
|
+
```
|
377
|
+
|
378
|
+
2. **Verify cache backend:**
|
379
|
+
```ruby
|
380
|
+
# Test cache is working
|
381
|
+
Rails.cache.write("test", "value")
|
382
|
+
puts Rails.cache.read("test") # Should output "value"
|
383
|
+
```
|
384
|
+
|
385
|
+
3. **Check session boundaries:**
|
386
|
+
```ruby
|
387
|
+
# Debug session ID generation
|
388
|
+
FlowChat.logger.level = Logger::DEBUG
|
389
|
+
# Look for "Session::Middleware: Generated session ID: ..." messages
|
390
|
+
```
|
391
|
+
|
392
|
+
### Different Session IDs
|
393
|
+
|
394
|
+
1. **Inconsistent request data:**
|
395
|
+
- Verify `request.gateway` is consistent
|
396
|
+
- Check `request.platform` is set correctly
|
397
|
+
- Ensure `request.msisdn` format is consistent
|
398
|
+
|
399
|
+
2. **Boundary configuration mismatch:**
|
400
|
+
```ruby
|
401
|
+
# Ensure same boundaries across requests
|
402
|
+
config.use_session_config(boundaries: [:flow, :platform])
|
403
|
+
```
|
404
|
+
|
405
|
+
### Session Data Lost
|
406
|
+
|
407
|
+
1. **Cache expiration:**
|
408
|
+
```ruby
|
409
|
+
# Increase TTL if needed
|
410
|
+
FlowChat::Config.cache = Rails.cache
|
411
|
+
# Configure longer expiration in cache store
|
412
|
+
```
|
413
|
+
|
414
|
+
2. **Session ID changes:**
|
415
|
+
- Check logs for "Generated session ID" messages
|
416
|
+
- Verify identifier consistency (`:msisdn` vs `:request_id`)
|
417
|
+
|
418
|
+
## Monitoring and Instrumentation
|
419
|
+
|
420
|
+
FlowChat emits events for session operations:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
# Subscribe to session events
|
424
|
+
ActiveSupport::Notifications.subscribe("session.created.flow_chat") do |event|
|
425
|
+
Rails.logger.info "Session created: #{event.payload[:session_id]}"
|
426
|
+
end
|
427
|
+
|
428
|
+
# Monitor session usage
|
429
|
+
ActiveSupport::Notifications.subscribe(/session\..*\.flow_chat/) do |name, start, finish, id, payload|
|
430
|
+
# Track session operations for analytics
|
431
|
+
Analytics.track("flowchat.session.#{name.split('.')[1]}", payload)
|
432
|
+
end
|
433
|
+
```
|
data/docs/testing.md
CHANGED
@@ -43,7 +43,7 @@ class SimulatorController < ApplicationController
|
|
43
43
|
name: "USSD Integration",
|
44
44
|
icon: "📱",
|
45
45
|
processor_type: "ussd",
|
46
|
-
|
46
|
+
gateway: "nalo",
|
47
47
|
endpoint: "/ussd",
|
48
48
|
color: "#007bff"
|
49
49
|
},
|
@@ -51,7 +51,7 @@ class SimulatorController < ApplicationController
|
|
51
51
|
name: "WhatsApp Integration",
|
52
52
|
icon: "💬",
|
53
53
|
processor_type: "whatsapp",
|
54
|
-
|
54
|
+
gateway: "cloud_api",
|
55
55
|
endpoint: "/whatsapp/webhook",
|
56
56
|
color: "#25D366"
|
57
57
|
}
|
data/docs/ussd-setup.md
CHANGED
@@ -119,19 +119,35 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor i
|
|
119
119
|
0 Back
|
120
120
|
```
|
121
121
|
|
122
|
-
##
|
122
|
+
## Session Management
|
123
123
|
|
124
|
-
|
124
|
+
Configure session behavior for better user experience:
|
125
125
|
|
126
126
|
```ruby
|
127
127
|
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
128
128
|
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
129
129
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
130
|
-
|
130
|
+
|
131
|
+
# Enable durable sessions (shorthand)
|
132
|
+
config.use_durable_sessions
|
131
133
|
end
|
132
134
|
```
|
133
135
|
|
134
|
-
|
136
|
+
### Session Boundaries
|
137
|
+
|
138
|
+
Session boundaries control how session IDs are constructed:
|
139
|
+
|
140
|
+
- **`:flow`** - Separate sessions per flow class
|
141
|
+
- **`:platform`** - Separate USSD from WhatsApp sessions
|
142
|
+
- **`:gateway`** - Separate sessions per gateway
|
143
|
+
- **`[]`** - Global sessions (no boundaries)
|
144
|
+
|
145
|
+
Session identifier options:
|
146
|
+
|
147
|
+
- **`nil`** - Platform chooses default (`:request_id` for USSD, `:msisdn` for WhatsApp)
|
148
|
+
- **`:msisdn`** - Use phone number (durable sessions)
|
149
|
+
- **`:request_id`** - Use request ID (ephemeral sessions)
|
150
|
+
- **`hash_phone_numbers`** - Hash phone numbers for privacy (recommended)
|
135
151
|
|
136
152
|
## Middleware
|
137
153
|
|
@@ -17,7 +17,7 @@ class SimulatorController < ApplicationController
|
|
17
17
|
name: "Main USSD Endpoint",
|
18
18
|
description: "Primary USSD integration",
|
19
19
|
processor_type: "ussd",
|
20
|
-
|
20
|
+
gateway: "nalo",
|
21
21
|
endpoint: "/ussd",
|
22
22
|
icon: "📱",
|
23
23
|
color: "#28a745"
|
@@ -26,7 +26,7 @@ class SimulatorController < ApplicationController
|
|
26
26
|
name: "Main WhatsApp Endpoint",
|
27
27
|
description: "Primary WhatsApp webhook",
|
28
28
|
processor_type: "whatsapp",
|
29
|
-
|
29
|
+
gateway: "cloud_api",
|
30
30
|
endpoint: "/whatsapp/webhook",
|
31
31
|
icon: "💬",
|
32
32
|
color: "#25D366"
|
@@ -35,7 +35,7 @@ class SimulatorController < ApplicationController
|
|
35
35
|
name: "Tenant A WhatsApp",
|
36
36
|
description: "Multi-tenant endpoint for Tenant A",
|
37
37
|
processor_type: "whatsapp",
|
38
|
-
|
38
|
+
gateway: "cloud_api",
|
39
39
|
endpoint: "/tenants/a/whatsapp/webhook",
|
40
40
|
icon: "🏢",
|
41
41
|
color: "#fd7e14"
|
@@ -44,7 +44,7 @@ class SimulatorController < ApplicationController
|
|
44
44
|
name: "Legacy WhatsApp",
|
45
45
|
description: "Legacy endpoint for compatibility",
|
46
46
|
processor_type: "whatsapp",
|
47
|
-
|
47
|
+
gateway: "cloud_api",
|
48
48
|
endpoint: "/legacy/whatsapp",
|
49
49
|
icon: "📦",
|
50
50
|
color: "#6c757d"
|
data/examples/ussd_controller.rb
CHANGED
@@ -10,8 +10,8 @@ class UssdController < ApplicationController
|
|
10
10
|
# Use Rails session for USSD (shorter sessions)
|
11
11
|
config.use_session_store FlowChat::Session::RailsSessionStore
|
12
12
|
|
13
|
-
# Enable
|
14
|
-
config.
|
13
|
+
# Enable durable sessions (optional)
|
14
|
+
config.use_durable_sessions # Configures flow+platform isolation with durable sessions
|
15
15
|
end
|
16
16
|
|
17
17
|
processor.run WelcomeFlow, :main_page
|
@@ -232,7 +232,13 @@ class UssdController < ApplicationController
|
|
232
232
|
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
233
233
|
config.use_session_store FlowChat::Session::RailsSessionStore
|
234
234
|
config.use_middleware LoggingMiddleware # Add custom logging
|
235
|
-
config.
|
235
|
+
config.use_durable_sessions # Enable durable sessions
|
236
|
+
|
237
|
+
# Or configure session boundaries explicitly:
|
238
|
+
# config.use_session_config(
|
239
|
+
# boundaries: [:flow, :platform], # which boundaries to enforce
|
240
|
+
# hash_phone_numbers: true # hash phone numbers for privacy
|
241
|
+
# )
|
236
242
|
end
|
237
243
|
|
238
244
|
processor.run WelcomeFlow, :main_page
|
@@ -13,6 +13,7 @@ module FlowChat
|
|
13
13
|
@context["controller"] = controller
|
14
14
|
@context["enable_simulator"] = enable_simulator.nil? ? (defined?(Rails) && Rails.env.local?) : enable_simulator
|
15
15
|
@middleware = ::Middleware::Builder.new(name: middleware_name)
|
16
|
+
@session_options = FlowChat::Config.session
|
16
17
|
|
17
18
|
FlowChat.logger.debug { "BaseProcessor: Simulator mode #{@context["enable_simulator"] ? "enabled" : "disabled"}" }
|
18
19
|
|
@@ -29,17 +30,46 @@ module FlowChat
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def use_session_store(session_store)
|
32
|
-
|
33
|
+
raise "Session store must be a class" unless session_store.is_a?(Class)
|
34
|
+
FlowChat.logger.debug { "BaseProcessor: Configuring session store #{session_store.name}" }
|
33
35
|
@context["session.store"] = session_store
|
34
36
|
self
|
35
37
|
end
|
36
38
|
|
39
|
+
def use_session_config(boundaries: nil, hash_phone_numbers: nil, identifier: nil)
|
40
|
+
FlowChat.logger.debug { "BaseProcessor: Configuring session config: boundaries=#{boundaries.inspect}, hash_phone_numbers=#{hash_phone_numbers}, identifier=#{identifier}" }
|
41
|
+
|
42
|
+
# Update the session options directly
|
43
|
+
@session_options = @session_options.dup
|
44
|
+
@session_options.boundaries = Array(boundaries) if boundaries
|
45
|
+
@session_options.hash_phone_numbers = hash_phone_numbers if hash_phone_numbers
|
46
|
+
@session_options.identifier = identifier if identifier
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
37
51
|
def use_middleware(middleware)
|
38
|
-
|
52
|
+
raise "Middleware must be a class" unless middleware.is_a?(Class)
|
53
|
+
FlowChat.logger.debug { "BaseProcessor: Adding middleware #{middleware.name}" }
|
39
54
|
@middleware.use middleware
|
40
55
|
self
|
41
56
|
end
|
42
57
|
|
58
|
+
def use_cross_platform_sessions
|
59
|
+
FlowChat.logger.debug { "BaseProcessor: Enabling cross-platform sessions via session configuration" }
|
60
|
+
use_session_config(
|
61
|
+
boundaries: [:flow]
|
62
|
+
)
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def use_url_isolation
|
67
|
+
FlowChat.logger.debug { "BaseProcessor: Enabling URL-based session isolation" }
|
68
|
+
current_boundaries = @session_options.boundaries.dup
|
69
|
+
current_boundaries << :url unless current_boundaries.include?(:url)
|
70
|
+
use_session_config(boundaries: current_boundaries)
|
71
|
+
end
|
72
|
+
|
43
73
|
def run(flow_class, action)
|
44
74
|
# Instrument flow execution (this will log via LogSubscriber)
|
45
75
|
instrument(Events::FLOW_EXECUTION_START, {
|
@@ -101,6 +131,7 @@ module FlowChat
|
|
101
131
|
|
102
132
|
::Middleware::Builder.new(name: name) do |b|
|
103
133
|
b.use @gateway_class, *@gateway_args
|
134
|
+
b.use FlowChat::Session::Middleware, @session_options
|
104
135
|
configure_middleware_stack(b)
|
105
136
|
end.inject_logger(FlowChat.logger)
|
106
137
|
end
|
data/lib/flow_chat/config.rb
CHANGED
@@ -8,6 +8,11 @@ module FlowChat
|
|
8
8
|
# When false, only the validation error message is shown to the user.
|
9
9
|
mattr_accessor :combine_validation_error_with_message, default: true
|
10
10
|
|
11
|
+
# Session configuration object
|
12
|
+
def self.session
|
13
|
+
@session ||= SessionConfig.new
|
14
|
+
end
|
15
|
+
|
11
16
|
# USSD-specific configuration object
|
12
17
|
def self.ussd
|
13
18
|
@ussd ||= UssdConfig.new
|
@@ -18,10 +23,29 @@ module FlowChat
|
|
18
23
|
@whatsapp ||= WhatsappConfig.new
|
19
24
|
end
|
20
25
|
|
26
|
+
class SessionConfig
|
27
|
+
attr_accessor :boundaries, :hash_phone_numbers, :identifier
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
# Session boundaries control how session IDs are constructed
|
31
|
+
# :flow = separate sessions per flow
|
32
|
+
# :gateway = separate sessions per gateway
|
33
|
+
# :platform = separate sessions per platform (ussd, whatsapp)
|
34
|
+
@boundaries = [:flow, :gateway, :platform]
|
35
|
+
|
36
|
+
# Always hash phone numbers for privacy
|
37
|
+
@hash_phone_numbers = true
|
38
|
+
|
39
|
+
# Session identifier type (nil = let platforms choose their default)
|
40
|
+
# :msisdn = durable sessions (durable across timeouts)
|
41
|
+
# :request_id = ephemeral sessions (new session each time)
|
42
|
+
@identifier = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
21
46
|
class UssdConfig
|
22
47
|
attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
|
23
|
-
:pagination_next_option, :pagination_next_text
|
24
|
-
:resumable_sessions_enabled, :resumable_sessions_global, :resumable_sessions_timeout_seconds
|
48
|
+
:pagination_next_option, :pagination_next_text
|
25
49
|
|
26
50
|
def initialize
|
27
51
|
@pagination_page_size = 140
|
@@ -29,9 +53,6 @@ module FlowChat
|
|
29
53
|
@pagination_back_text = "Back"
|
30
54
|
@pagination_next_option = "#"
|
31
55
|
@pagination_next_text = "More"
|
32
|
-
@resumable_sessions_enabled = false
|
33
|
-
@resumable_sessions_global = true
|
34
|
-
@resumable_sessions_timeout_seconds = 300
|
35
56
|
end
|
36
57
|
end
|
37
58
|
|
@@ -5,8 +5,9 @@ module FlowChat
|
|
5
5
|
|
6
6
|
attr_reader :context
|
7
7
|
|
8
|
-
def initialize(app)
|
8
|
+
def initialize(app, session_options)
|
9
9
|
@app = app
|
10
|
+
@session_options = session_options
|
10
11
|
FlowChat.logger.debug { "Session::Middleware: Initialized session middleware" }
|
11
12
|
end
|
12
13
|
|
@@ -19,9 +20,10 @@ module FlowChat
|
|
19
20
|
context.session = context["session.store"].new(context)
|
20
21
|
|
21
22
|
# Use instrumentation instead of direct logging for session creation
|
23
|
+
store_type = context["session.store"].name || "$Anonymous"
|
22
24
|
instrument(Events::SESSION_CREATED, {
|
23
25
|
session_id: session_id,
|
24
|
-
store_type:
|
26
|
+
store_type: store_type,
|
25
27
|
gateway: context["request.gateway"]
|
26
28
|
})
|
27
29
|
|
@@ -40,28 +42,111 @@ module FlowChat
|
|
40
42
|
|
41
43
|
def session_id(context)
|
42
44
|
gateway = context["request.gateway"]
|
45
|
+
platform = context["request.platform"]
|
43
46
|
flow_name = context["flow.name"]
|
44
47
|
|
45
|
-
|
48
|
+
# Check for explicit session ID first (for manual session management)
|
49
|
+
if context["session.id"].present?
|
50
|
+
session_id = context["session.id"]
|
51
|
+
FlowChat.logger.debug { "Session::Middleware: Using explicit session ID: #{session_id}" }
|
52
|
+
return session_id
|
53
|
+
end
|
54
|
+
|
55
|
+
FlowChat.logger.debug { "Session::Middleware: Building session ID for platform=#{platform}, gateway=#{gateway}, flow=#{flow_name}" }
|
46
56
|
|
47
|
-
|
48
|
-
|
49
|
-
|
57
|
+
# Get identifier based on configuration
|
58
|
+
identifier = get_session_identifier(context)
|
59
|
+
|
60
|
+
# Build session ID based on configuration
|
61
|
+
session_id = build_session_id(flow_name, platform, gateway, identifier)
|
62
|
+
FlowChat.logger.debug { "Session::Middleware: Generated session ID: #{session_id}" }
|
63
|
+
session_id
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_session_identifier(context)
|
67
|
+
identifier_type = @session_options.identifier
|
68
|
+
|
69
|
+
# If no identifier specified, use platform defaults
|
70
|
+
if identifier_type.nil?
|
71
|
+
platform = context["request.platform"]
|
72
|
+
identifier_type = case platform
|
73
|
+
when :ussd
|
74
|
+
:request_id # USSD defaults to ephemeral sessions
|
75
|
+
when :whatsapp
|
76
|
+
:msisdn # WhatsApp defaults to durable sessions
|
77
|
+
else
|
78
|
+
:msisdn # Default fallback to durable
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
case identifier_type
|
83
|
+
when :request_id
|
84
|
+
context["request.id"]
|
85
|
+
when :msisdn
|
50
86
|
phone = context["request.msisdn"]
|
51
|
-
|
52
|
-
FlowChat.logger.debug { "Session::Middleware: WhatsApp session ID created for phone #{phone}" }
|
53
|
-
session_id
|
54
|
-
# when :nalo, :nsano
|
55
|
-
# # For USSD, use the request ID from the gateway
|
56
|
-
# "#{gateway}:#{flow_name}:#{context["request.id"]}"
|
87
|
+
@session_options.hash_phone_numbers ? hash_phone_number(phone) : phone
|
57
88
|
else
|
58
|
-
|
59
|
-
request_id = context["request.id"]
|
60
|
-
session_id = "#{gateway}:#{flow_name}:#{request_id}"
|
61
|
-
FlowChat.logger.debug { "Session::Middleware: Generic session ID created for request #{request_id}" }
|
62
|
-
session_id
|
89
|
+
raise "Invalid session identifier type: #{identifier_type}"
|
63
90
|
end
|
64
91
|
end
|
92
|
+
|
93
|
+
def build_session_id(flow_name, platform, gateway, identifier)
|
94
|
+
parts = []
|
95
|
+
|
96
|
+
# Add flow name if flow isolation is enabled
|
97
|
+
parts << flow_name if @session_options.boundaries.include?(:flow)
|
98
|
+
|
99
|
+
# Add platform if platform isolation is enabled
|
100
|
+
parts << platform.to_s if @session_options.boundaries.include?(:platform)
|
101
|
+
|
102
|
+
# Add gateway if gateway isolation is enabled
|
103
|
+
parts << gateway.to_s if @session_options.boundaries.include?(:gateway)
|
104
|
+
|
105
|
+
# Add URL if URL isolation is enabled
|
106
|
+
if @session_options.boundaries.include?(:url)
|
107
|
+
url_identifier = get_url_identifier(context)
|
108
|
+
parts << url_identifier if url_identifier.present?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Add the session identifier
|
112
|
+
parts << identifier if identifier.present?
|
113
|
+
|
114
|
+
# Join parts with colons
|
115
|
+
parts.join(":")
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_url_identifier(context)
|
119
|
+
request = context.controller&.request
|
120
|
+
return nil unless request
|
121
|
+
|
122
|
+
# Extract host and path for URL boundary
|
123
|
+
host = request.host rescue nil
|
124
|
+
path = request.path rescue nil
|
125
|
+
|
126
|
+
# Create a normalized URL identifier: host + path
|
127
|
+
# e.g., "example.com/api/v1/ussd" or "tenant1.example.com/ussd"
|
128
|
+
url_parts = []
|
129
|
+
url_parts << host if host.present?
|
130
|
+
url_parts << path.sub(/^\//, '') if path.present? && path != '/'
|
131
|
+
|
132
|
+
# For long URLs, use first part + hash suffix instead of full hash
|
133
|
+
url_identifier = url_parts.join('/').gsub(/[^a-zA-Z0-9._-]/, '_')
|
134
|
+
if url_identifier.length > 50
|
135
|
+
require 'digest'
|
136
|
+
# Take first 41 chars + hash suffix to keep it manageable but recognizable
|
137
|
+
first_part = url_identifier[0, 41]
|
138
|
+
hash_suffix = Digest::SHA256.hexdigest(url_identifier)[0, 8]
|
139
|
+
url_identifier = "#{first_part}_#{hash_suffix}"
|
140
|
+
end
|
141
|
+
|
142
|
+
url_identifier
|
143
|
+
end
|
144
|
+
|
145
|
+
def hash_phone_number(phone)
|
146
|
+
# Use SHA256 but only take first 8 characters for reasonable session IDs
|
147
|
+
require 'digest'
|
148
|
+
Digest::SHA256.hexdigest(phone.to_s)[0, 8]
|
149
|
+
end
|
65
150
|
end
|
66
151
|
end
|
67
152
|
end
|
@@ -32,7 +32,7 @@ module FlowChat
|
|
32
32
|
name: "USSD (Nalo)",
|
33
33
|
description: "USSD integration using Nalo",
|
34
34
|
processor_type: "ussd",
|
35
|
-
|
35
|
+
gateway: "nalo",
|
36
36
|
endpoint: "/ussd",
|
37
37
|
icon: "📱",
|
38
38
|
color: "#28a745",
|
@@ -45,7 +45,7 @@ module FlowChat
|
|
45
45
|
name: "WhatsApp (Cloud API)",
|
46
46
|
description: "WhatsApp integration using Cloud API",
|
47
47
|
processor_type: "whatsapp",
|
48
|
-
|
48
|
+
gateway: "cloud_api",
|
49
49
|
endpoint: "/whatsapp/webhook",
|
50
50
|
icon: "💬",
|
51
51
|
color: "#25D366",
|
@@ -1057,8 +1057,8 @@
|
|
1057
1057
|
<span class="config-detail-value">${config.endpoint}</span>
|
1058
1058
|
</div>
|
1059
1059
|
<div class="config-detail-item">
|
1060
|
-
<span class="config-detail-label">
|
1061
|
-
<span class="config-detail-value">${config.
|
1060
|
+
<span class="config-detail-label">gateway:</span>
|
1061
|
+
<span class="config-detail-value">${config.gateway}</span>
|
1062
1062
|
</div>
|
1063
1063
|
<div class="config-detail-item">
|
1064
1064
|
<span class="config-detail-label">Type:</span>
|
@@ -1304,7 +1304,7 @@
|
|
1304
1304
|
|
1305
1305
|
let requestData = {}
|
1306
1306
|
|
1307
|
-
switch (config.
|
1307
|
+
switch (config.gateway) {
|
1308
1308
|
case 'nalo':
|
1309
1309
|
requestData = {
|
1310
1310
|
USERID: state.sessionId,
|
@@ -1322,7 +1322,7 @@
|
|
1322
1322
|
}
|
1323
1323
|
break
|
1324
1324
|
default:
|
1325
|
-
throw new Error(`Unsupported USSD
|
1325
|
+
throw new Error(`Unsupported USSD gateway: ${config.gateway}`)
|
1326
1326
|
}
|
1327
1327
|
|
1328
1328
|
try {
|
@@ -1342,7 +1342,7 @@
|
|
1342
1342
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
1343
1343
|
}
|
1344
1344
|
|
1345
|
-
switch (config.
|
1345
|
+
switch (config.gateway) {
|
1346
1346
|
case 'nalo':
|
1347
1347
|
displayUSSDResponse(data.MSG)
|
1348
1348
|
state.isRunning = data.MSGTYPE
|
@@ -20,6 +20,7 @@ module FlowChat
|
|
20
20
|
context["request.message_id"] = SecureRandom.uuid
|
21
21
|
context["request.timestamp"] = Time.current.iso8601
|
22
22
|
context["request.gateway"] = :nalo
|
23
|
+
context["request.platform"] = :ussd
|
23
24
|
context["request.network"] = nil
|
24
25
|
context["request.msisdn"] = Phonelib.parse(params["MSISDN"]).e164
|
25
26
|
# context["request.type"] = params["MSGTYPE"] ? :initial : :response
|
@@ -22,6 +22,7 @@ module FlowChat
|
|
22
22
|
|
23
23
|
# Set a basic message_id (can be enhanced based on actual Nsano implementation)
|
24
24
|
context["request.message_id"] = SecureRandom.uuid
|
25
|
+
context["request.platform"] = :ussd
|
25
26
|
|
26
27
|
# TODO: Implement Nsano-specific parameter parsing
|
27
28
|
# For now, add basic instrumentation structure for when this is implemented
|
@@ -58,7 +59,7 @@ module FlowChat
|
|
58
59
|
# if params["network"].present? && params["UserSessionID"].present?
|
59
60
|
# request_id = "nsano::request_id::#{params["UserSessionID"]}"
|
60
61
|
# context["ussd.request"] = {
|
61
|
-
#
|
62
|
+
# gateway: :nsano,
|
62
63
|
# network: params["network"].to_sym,
|
63
64
|
# msisdn: Phonelib.parse(params["msisdn"]).e164,
|
64
65
|
# type: Config.cache&.read(request_id).present? ? :response : :initial,
|
@@ -70,7 +71,7 @@ module FlowChat
|
|
70
71
|
|
71
72
|
# status, headers, response = @app.call(context)
|
72
73
|
|
73
|
-
# if context["ussd.response"].present? && context["ussd.request"][:
|
74
|
+
# if context["ussd.response"].present? && context["ussd.request"][:gateway] == :nsano
|
74
75
|
# if context["ussd.response"][:type] == :terminal
|
75
76
|
# Config.cache&.write(request_id, nil)
|
76
77
|
# else
|
@@ -1,10 +1,11 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Ussd
|
3
3
|
class Processor < FlowChat::BaseProcessor
|
4
|
-
def
|
5
|
-
FlowChat.logger.debug { "Ussd::Processor: Enabling
|
6
|
-
|
7
|
-
|
4
|
+
def use_durable_sessions(cross_gateway: false)
|
5
|
+
FlowChat.logger.debug { "Ussd::Processor: Enabling durable sessions via session configuration" }
|
6
|
+
use_session_config(
|
7
|
+
identifier: :msisdn # Use MSISDN for durable sessions
|
8
|
+
)
|
8
9
|
end
|
9
10
|
|
10
11
|
protected
|
@@ -21,9 +22,6 @@ module FlowChat
|
|
21
22
|
def configure_middleware_stack(builder)
|
22
23
|
FlowChat.logger.debug { "Ussd::Processor: Configuring USSD middleware stack" }
|
23
24
|
|
24
|
-
builder.use FlowChat::Session::Middleware
|
25
|
-
FlowChat.logger.debug { "Ussd::Processor: Added Session::Middleware" }
|
26
|
-
|
27
25
|
builder.use FlowChat::Ussd::Middleware::Pagination
|
28
26
|
FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Pagination" }
|
29
27
|
|
data/lib/flow_chat/version.rb
CHANGED
@@ -160,6 +160,7 @@ module FlowChat
|
|
160
160
|
|
161
161
|
context["request.id"] = phone_number
|
162
162
|
context["request.gateway"] = :whatsapp_cloud_api
|
163
|
+
context["request.platform"] = :whatsapp
|
163
164
|
context["request.message_id"] = message_id
|
164
165
|
context["request.msisdn"] = Phonelib.parse(phone_number).e164
|
165
166
|
context["request.contact_name"] = contact_name
|
@@ -20,8 +20,6 @@ module FlowChat
|
|
20
20
|
|
21
21
|
def configure_middleware_stack(builder)
|
22
22
|
FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp middleware stack" }
|
23
|
-
builder.use FlowChat::Session::Middleware
|
24
|
-
FlowChat.logger.debug { "Whatsapp::Processor: Added Session::Middleware" }
|
25
23
|
|
26
24
|
builder.use middleware
|
27
25
|
FlowChat.logger.debug { "Whatsapp::Processor: Added custom middleware" }
|
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.8.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-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- docs/images/simulator.png
|
108
108
|
- docs/instrumentation.md
|
109
109
|
- docs/media.md
|
110
|
+
- docs/sessions.md
|
110
111
|
- docs/testing.md
|
111
112
|
- docs/ussd-setup.md
|
112
113
|
- docs/whatsapp-setup.md
|
@@ -139,7 +140,6 @@ files:
|
|
139
140
|
- lib/flow_chat/ussd/middleware/choice_mapper.rb
|
140
141
|
- lib/flow_chat/ussd/middleware/executor.rb
|
141
142
|
- lib/flow_chat/ussd/middleware/pagination.rb
|
142
|
-
- lib/flow_chat/ussd/middleware/resumable_session.rb
|
143
143
|
- lib/flow_chat/ussd/processor.rb
|
144
144
|
- lib/flow_chat/ussd/renderer.rb
|
145
145
|
- lib/flow_chat/version.rb
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module FlowChat
|
2
|
-
module Ussd
|
3
|
-
module Middleware
|
4
|
-
class ResumableSession
|
5
|
-
def initialize(app)
|
6
|
-
@app = app
|
7
|
-
end
|
8
|
-
|
9
|
-
def call(context)
|
10
|
-
if FlowChat::Config.ussd.resumable_sessions_enabled && context["ussd.request"].present?
|
11
|
-
# First, try to find any interruption session.
|
12
|
-
# The session key can be:
|
13
|
-
# - a global session (key: "global")
|
14
|
-
# - a provider-specific session (key: <provider>)
|
15
|
-
session_key = self.class.session_key(context)
|
16
|
-
resumable_session = context["session.store"].get(session_key)
|
17
|
-
|
18
|
-
if resumable_session.present? && valid?(resumable_session)
|
19
|
-
context.merge! resumable_session
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
@app.call(context)
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def valid?(session)
|
29
|
-
return true unless FlowChat::Config.ussd.resumable_sessions_timeout_seconds
|
30
|
-
|
31
|
-
last_active_at = Time.parse session.dig("context", "last_active_at")
|
32
|
-
(Time.current - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
|
33
|
-
rescue
|
34
|
-
false
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|