flow_chat 0.2.0 โ 0.3.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/.DS_Store +0 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -1
- data/README.md +604 -14
- data/Rakefile +7 -3
- data/flow_chat.gemspec +2 -2
- data/images/ussd_simulator.png +0 -0
- data/lib/flow_chat/config.rb +21 -8
- data/lib/flow_chat/context.rb +8 -0
- data/lib/flow_chat/flow.rb +9 -0
- data/lib/flow_chat/interrupt.rb +2 -0
- data/lib/flow_chat/session/middleware.rb +1 -1
- data/lib/flow_chat/ussd/app.rb +1 -1
- data/lib/flow_chat/ussd/gateway/nalo.rb +1 -1
- data/lib/flow_chat/ussd/gateway/nsano.rb +1 -1
- data/lib/flow_chat/ussd/middleware/pagination.rb +14 -17
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +18 -31
- data/lib/flow_chat/ussd/prompt.rb +1 -1
- data/lib/flow_chat/ussd/simulator/controller.rb +1 -1
- data/lib/flow_chat/ussd/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/version.rb +1 -1
- metadata +9 -6
- data/.rspec +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31c7630d410bbcff17307ade16634f5d5c9662f91eeeab8365b98681886de6b3
|
4
|
+
data.tar.gz: 4a7d4059a7a60dee5af2b189fa33c4638a439af903a72f38ffa5a67f9325115d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6040ccd725fa1a24b7ff46b5d288e11a1f1a4a2d58b4e773a843153a7d2cf3fb7c1fd7971dd27ed2bc938bf12da17a42b9bdbee9db06152507051eca57592fdb
|
7
|
+
data.tar.gz: 002d2fc1f5b6ea0b20e4229220801bc94311b5237ce24891a571f6f544abaf072a6fdf71de15632069a40d1b29b58e349f937b80ff3975f97bd3a607053b2c39
|
data/.DS_Store
ADDED
Binary file
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.2
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,39 +1,629 @@
|
|
1
1
|
# FlowChat
|
2
2
|
|
3
|
-
|
3
|
+
FlowChat is a Rails framework designed for building sophisticated conversational workflows, particularly for USSD (Unstructured Supplementary Service Data) systems. It provides an intuitive Ruby DSL for creating multi-step, menu-driven conversations with automatic session management, input validation, and flow control.
|
4
4
|
|
5
|
-
|
5
|
+
**Key Features:**
|
6
|
+
- ๐ฏ **Declarative Flow Definition** - Define conversation flows as Ruby classes
|
7
|
+
- ๐ **Automatic Session Management** - Persistent state across requests
|
8
|
+
- โ
**Input Validation & Transformation** - Built-in validation and data conversion
|
9
|
+
- ๐ **Middleware Architecture** - Flexible request processing pipeline
|
10
|
+
- ๐ฑ **USSD Gateway Support** - Currently supports Nalo and Nsano gateways
|
11
|
+
- ๐งช **Built-in Testing Tools** - USSD simulator for local development
|
12
|
+
|
13
|
+
## Architecture Overview
|
14
|
+
|
15
|
+
FlowChat uses a **request-per-interaction** model where each user input creates a new request. The framework maintains conversation state through session storage while processing each interaction through a middleware pipeline.
|
16
|
+
|
17
|
+
```
|
18
|
+
User Input โ Gateway โ Session โ Pagination โ Custom โ Executor โ Flow โ Response
|
19
|
+
โ
|
20
|
+
Session Storage
|
21
|
+
```
|
22
|
+
|
23
|
+
**Middleware Pipeline:**
|
24
|
+
- **Gateway**: USSD provider communication (Nalo/Nsano)
|
25
|
+
- **Session**: Load/save conversation state
|
26
|
+
- **Pagination**: Split long responses into pages
|
27
|
+
- **Custom**: Your application middleware (logging, auth, etc.)
|
28
|
+
- **Executor**: Execute flow methods and handle interrupts
|
6
29
|
|
7
30
|
## Installation
|
8
31
|
|
9
|
-
Add
|
32
|
+
Add FlowChat to your Rails application's Gemfile:
|
10
33
|
|
11
34
|
```ruby
|
12
35
|
gem 'flow_chat'
|
13
36
|
```
|
14
37
|
|
15
|
-
|
38
|
+
Then execute:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
bundle install
|
42
|
+
```
|
43
|
+
|
44
|
+
## Quick Start
|
45
|
+
|
46
|
+
### 1. Create Your First Flow
|
47
|
+
|
48
|
+
Create a flow class in `app/flow_chat/welcome_flow.rb`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class WelcomeFlow < FlowChat::Flow
|
52
|
+
def main_page
|
53
|
+
name = app.screen(:name) do |prompt|
|
54
|
+
prompt.ask "Welcome! What's your name?",
|
55
|
+
transform: ->(input) { input.strip.titleize }
|
56
|
+
end
|
57
|
+
|
58
|
+
app.say "Hello, #{name}! Welcome to FlowChat."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### 2. Set Up the Controller
|
64
|
+
|
65
|
+
Create a controller to handle USSD requests:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class UssdController < ApplicationController
|
69
|
+
skip_forgery_protection
|
70
|
+
|
71
|
+
def process_request
|
72
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
73
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
74
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
75
|
+
end
|
76
|
+
|
77
|
+
processor.run WelcomeFlow, :main_page
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### 3. Configure Routes
|
83
|
+
|
84
|
+
Add the route to `config/routes.rb`:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
Rails.application.routes.draw do
|
88
|
+
post 'ussd' => 'ussd#process_request'
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
## Core Concepts
|
93
|
+
|
94
|
+
### Flows and Screens
|
95
|
+
|
96
|
+
**Flows** are Ruby classes that define conversation logic. **Screens** represent individual interaction points where you collect user input.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
class RegistrationFlow < FlowChat::Flow
|
100
|
+
def main_page
|
101
|
+
# Each screen captures one piece of user input
|
102
|
+
phone = app.screen(:phone) do |prompt|
|
103
|
+
prompt.ask "Enter your phone number:",
|
104
|
+
validate: ->(input) { "Invalid phone number" unless valid_phone?(input) }
|
105
|
+
end
|
106
|
+
|
107
|
+
age = app.screen(:age) do |prompt|
|
108
|
+
prompt.ask "Enter your age:",
|
109
|
+
convert: ->(input) { input.to_i },
|
110
|
+
validate: ->(input) { "Must be 18 or older" unless input >= 18 }
|
111
|
+
end
|
112
|
+
|
113
|
+
# Process the collected data
|
114
|
+
create_user(phone: phone, age: age)
|
115
|
+
app.say "Registration complete!"
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def valid_phone?(phone)
|
121
|
+
phone.match?(/\A\+?[\d\s\-\(\)]+\z/)
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_user(phone:, age:)
|
125
|
+
# Your user creation logic here
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
### Input Validation and Transformation
|
131
|
+
|
132
|
+
FlowChat provides powerful input processing capabilities:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
app.screen(:email) do |prompt|
|
136
|
+
prompt.ask "Enter your email:",
|
137
|
+
# Transform input before validation
|
138
|
+
transform: ->(input) { input.strip.downcase },
|
139
|
+
|
140
|
+
# Validate the input
|
141
|
+
validate: ->(input) {
|
142
|
+
"Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
143
|
+
},
|
144
|
+
|
145
|
+
# Convert to final format
|
146
|
+
convert: ->(input) { input }
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
### Menu Selection
|
151
|
+
|
152
|
+
Create selection menus with automatic validation:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# Array-based choices
|
156
|
+
language = app.screen(:language) do |prompt|
|
157
|
+
prompt.select "Choose your language:", ["English", "French", "Spanish"]
|
158
|
+
end
|
159
|
+
|
160
|
+
# Hash-based choices (keys are returned values)
|
161
|
+
plan = app.screen(:plan) do |prompt|
|
162
|
+
prompt.select "Choose a plan:", {
|
163
|
+
"basic" => "Basic Plan ($10/month)",
|
164
|
+
"premium" => "Premium Plan ($25/month)",
|
165
|
+
"enterprise" => "Enterprise Plan ($100/month)"
|
166
|
+
}
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
### Yes/No Prompts
|
171
|
+
|
172
|
+
Simplified boolean input collection:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
confirmed = app.screen(:confirmation) do |prompt|
|
176
|
+
prompt.yes? "Do you want to proceed with the payment?"
|
177
|
+
end
|
178
|
+
|
179
|
+
if confirmed
|
180
|
+
process_payment
|
181
|
+
app.say "Payment processed successfully!"
|
182
|
+
else
|
183
|
+
app.say "Payment cancelled."
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
## Advanced Features
|
188
|
+
|
189
|
+
### Session Management and Flow State
|
190
|
+
|
191
|
+
FlowChat automatically manages session state across requests. Each screen's result is cached, so users can navigate back and forth without losing data:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class OrderFlow < FlowChat::Flow
|
195
|
+
def main_page
|
196
|
+
# These values persist across requests
|
197
|
+
product = app.screen(:product) { |p| p.select "Choose product:", products }
|
198
|
+
quantity = app.screen(:quantity) { |p| p.ask "Quantity:", convert: :to_i }
|
199
|
+
|
200
|
+
# Show summary
|
201
|
+
total = calculate_total(product, quantity)
|
202
|
+
confirmed = app.screen(:confirm) do |prompt|
|
203
|
+
prompt.yes? "Order #{quantity}x #{product} for $#{total}. Confirm?"
|
204
|
+
end
|
205
|
+
|
206
|
+
if confirmed
|
207
|
+
process_order(product, quantity)
|
208
|
+
app.say "Order placed successfully!"
|
209
|
+
else
|
210
|
+
app.say "Order cancelled."
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
### Error Handling
|
217
|
+
|
218
|
+
Handle validation errors gracefully:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
app.screen(:credit_card) do |prompt|
|
222
|
+
prompt.ask "Enter credit card number:",
|
223
|
+
validate: ->(input) {
|
224
|
+
return "Card number must be 16 digits" unless input.length == 16
|
225
|
+
return "Invalid card number" unless luhn_valid?(input)
|
226
|
+
nil # Return nil for valid input
|
227
|
+
}
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
### Middleware Configuration
|
232
|
+
|
233
|
+
FlowChat uses a **middleware architecture** to process USSD requests through a configurable pipeline. Each request flows through multiple middleware layers in a specific order.
|
234
|
+
|
235
|
+
#### Default Middleware Stack
|
236
|
+
|
237
|
+
When you run a flow, FlowChat automatically builds this middleware stack:
|
238
|
+
|
239
|
+
```
|
240
|
+
User Input โ Gateway โ Session โ Pagination โ Custom Middleware โ Executor โ Flow
|
241
|
+
```
|
242
|
+
|
243
|
+
1. **Gateway Middleware** - Handles USSD provider communication (Nalo/Nsano)
|
244
|
+
2. **Session Middleware** - Manages session storage and retrieval
|
245
|
+
3. **Pagination Middleware** - Automatically splits long responses across pages
|
246
|
+
4. **Custom Middleware** - Your application-specific middleware (optional)
|
247
|
+
5. **Executor Middleware** - Executes the actual flow logic
|
248
|
+
|
249
|
+
#### Basic Configuration
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
253
|
+
# Gateway configuration (required)
|
254
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
255
|
+
|
256
|
+
# Session storage (required)
|
257
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
258
|
+
|
259
|
+
# Add custom middleware (optional)
|
260
|
+
config.use_middleware MyLoggingMiddleware
|
261
|
+
|
262
|
+
# Enable resumable sessions (optional)
|
263
|
+
config.use_resumable_sessions
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
#### Runtime Middleware Modification
|
268
|
+
|
269
|
+
You can modify the middleware stack at runtime for advanced use cases:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
processor.run(MyFlow, :main_page) do |stack|
|
273
|
+
# Add authentication middleware
|
274
|
+
stack.use AuthenticationMiddleware
|
275
|
+
|
276
|
+
# Insert rate limiting before execution
|
277
|
+
stack.insert_before FlowChat::Ussd::Middleware::Executor, RateLimitMiddleware
|
278
|
+
|
279
|
+
# Add logging after gateway
|
280
|
+
stack.insert_after gateway, RequestLoggingMiddleware
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
#### Built-in Middleware
|
285
|
+
|
286
|
+
**Pagination Middleware** automatically handles responses longer than 182 characters (configurable):
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
# Configure pagination behavior
|
290
|
+
FlowChat::Config.ussd.pagination_page_size = 140 # Default: 140 characters
|
291
|
+
FlowChat::Config.ussd.pagination_next_option = "#" # Default: "#"
|
292
|
+
FlowChat::Config.ussd.pagination_next_text = "More" # Default: "More"
|
293
|
+
FlowChat::Config.ussd.pagination_back_option = "0" # Default: "0"
|
294
|
+
FlowChat::Config.ussd.pagination_back_text = "Back" # Default: "Back"
|
295
|
+
```
|
296
|
+
|
297
|
+
**Resumable Sessions** allow users to continue interrupted conversations:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
301
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
302
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
303
|
+
config.use_resumable_sessions # Enable resumable sessions
|
304
|
+
end
|
305
|
+
```
|
306
|
+
|
307
|
+
#### Creating Custom Middleware
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
class LoggingMiddleware
|
311
|
+
def initialize(app)
|
312
|
+
@app = app
|
313
|
+
end
|
314
|
+
|
315
|
+
def call(context)
|
316
|
+
Rails.logger.info "Processing USSD request: #{context.input}"
|
317
|
+
|
318
|
+
# Call the next middleware in the stack
|
319
|
+
result = @app.call(context)
|
320
|
+
|
321
|
+
Rails.logger.info "Response: #{result[1]}"
|
322
|
+
result
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Use your custom middleware
|
327
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
328
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
329
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
330
|
+
config.use_middleware LoggingMiddleware
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
### Multiple Gateways
|
335
|
+
|
336
|
+
FlowChat supports multiple USSD gateways:
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
# Nalo Solutions Gateway
|
340
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
341
|
+
|
342
|
+
# Nsano Gateway
|
343
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nsano
|
344
|
+
```
|
345
|
+
|
346
|
+
## Testing
|
347
|
+
|
348
|
+
### Unit Testing Flows
|
349
|
+
|
350
|
+
Test your flows in isolation using the provided test helpers:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
require 'test_helper'
|
354
|
+
|
355
|
+
class WelcomeFlowTest < Minitest::Test
|
356
|
+
def setup
|
357
|
+
@context = FlowChat::Context.new
|
358
|
+
@context.session = create_test_session_store
|
359
|
+
end
|
360
|
+
|
361
|
+
def test_welcome_flow_with_name
|
362
|
+
@context.input = "John Doe"
|
363
|
+
app = FlowChat::Ussd::App.new(@context)
|
364
|
+
|
365
|
+
error = assert_raises(FlowChat::Interrupt::Terminate) do
|
366
|
+
flow = WelcomeFlow.new(app)
|
367
|
+
flow.main_page
|
368
|
+
end
|
369
|
+
|
370
|
+
assert_equal "Hello, John Doe! Welcome to FlowChat.", error.prompt
|
371
|
+
end
|
372
|
+
|
373
|
+
def test_welcome_flow_without_input
|
374
|
+
@context.input = nil
|
375
|
+
app = FlowChat::Ussd::App.new(@context)
|
376
|
+
|
377
|
+
error = assert_raises(FlowChat::Interrupt::Prompt) do
|
378
|
+
flow = WelcomeFlow.new(app)
|
379
|
+
flow.main_page
|
380
|
+
end
|
381
|
+
|
382
|
+
assert_equal "Welcome! What's your name?", error.prompt
|
383
|
+
end
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
### Integration Testing
|
388
|
+
|
389
|
+
Test complete user journeys:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
class RegistrationFlowIntegrationTest < Minitest::Test
|
393
|
+
def test_complete_registration_flow
|
394
|
+
controller = mock_controller
|
395
|
+
processor = FlowChat::Ussd::Processor.new(controller) do |config|
|
396
|
+
config.use_gateway MockGateway
|
397
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
398
|
+
end
|
399
|
+
|
400
|
+
# Simulate the complete flow
|
401
|
+
# First request - ask for phone
|
402
|
+
# Second request - provide phone, ask for age
|
403
|
+
# Third request - provide age, complete registration
|
404
|
+
end
|
405
|
+
end
|
406
|
+
```
|
407
|
+
|
408
|
+
### Testing Middleware
|
409
|
+
|
410
|
+
Test your custom middleware in isolation:
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
class LoggingMiddlewareTest < Minitest::Test
|
414
|
+
def test_logs_request_and_response
|
415
|
+
# Mock the next app in the chain
|
416
|
+
app = lambda { |context| [:prompt, "Test response", []] }
|
417
|
+
middleware = LoggingMiddleware.new(app)
|
418
|
+
|
419
|
+
context = FlowChat::Context.new
|
420
|
+
context.input = "test input"
|
421
|
+
|
422
|
+
# Capture log output
|
423
|
+
log_output = StringIO.new
|
424
|
+
Rails.stub(:logger, Logger.new(log_output)) do
|
425
|
+
type, prompt, choices = middleware.call(context)
|
426
|
+
|
427
|
+
assert_equal :prompt, type
|
428
|
+
assert_equal "Test response", prompt
|
429
|
+
assert_includes log_output.string, "Processing USSD request: test input"
|
430
|
+
assert_includes log_output.string, "Response: Test response"
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
```
|
435
|
+
|
436
|
+
### Testing Middleware Stack Modification
|
437
|
+
|
438
|
+
Test runtime middleware modifications:
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
class ProcessorMiddlewareTest < Minitest::Test
|
442
|
+
def test_custom_middleware_insertion
|
443
|
+
controller = mock_controller
|
444
|
+
processor = FlowChat::Ussd::Processor.new(controller) do |config|
|
445
|
+
config.use_gateway MockGateway
|
446
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
447
|
+
end
|
448
|
+
|
449
|
+
custom_middleware_called = false
|
450
|
+
custom_middleware = Class.new do
|
451
|
+
define_method(:initialize) { |app| @app = app }
|
452
|
+
define_method(:call) do |context|
|
453
|
+
custom_middleware_called = true
|
454
|
+
@app.call(context)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
processor.run(TestFlow, :main_page) do |stack|
|
459
|
+
stack.use custom_middleware
|
460
|
+
stack.insert_before FlowChat::Ussd::Middleware::Executor, custom_middleware
|
461
|
+
end
|
462
|
+
|
463
|
+
assert custom_middleware_called, "Custom middleware should have been executed"
|
464
|
+
end
|
465
|
+
end
|
466
|
+
```
|
467
|
+
|
468
|
+
### USSD Simulator
|
469
|
+
|
470
|
+
Use the built-in simulator for interactive testing:
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
class UssdSimulatorController < ApplicationController
|
474
|
+
include FlowChat::Ussd::Simulator::Controller
|
475
|
+
|
476
|
+
protected
|
477
|
+
|
478
|
+
def default_endpoint
|
479
|
+
'/ussd'
|
480
|
+
end
|
481
|
+
|
482
|
+
def default_provider
|
483
|
+
:nalo
|
484
|
+
end
|
485
|
+
end
|
486
|
+
```
|
487
|
+
|
488
|
+
Add to routes and visit `http://localhost:3000/ussd_simulator`.
|
489
|
+
|
490
|
+
## Best Practices
|
491
|
+
|
492
|
+
### 1. Keep Flows Focused
|
493
|
+
|
494
|
+
Create separate flows for different user journeys:
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
# Good: Focused flows
|
498
|
+
class LoginFlow < FlowChat::Flow
|
499
|
+
# Handle user authentication
|
500
|
+
end
|
501
|
+
|
502
|
+
class RegistrationFlow < FlowChat::Flow
|
503
|
+
# Handle user registration
|
504
|
+
end
|
505
|
+
|
506
|
+
class AccountFlow < FlowChat::Flow
|
507
|
+
# Handle account management
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
### 2. Use Descriptive Screen Names
|
512
|
+
|
513
|
+
Screen names should clearly indicate their purpose:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
# Good
|
517
|
+
app.screen(:customer_phone_number) { |p| p.ask "Phone:" }
|
518
|
+
app.screen(:payment_confirmation) { |p| p.yes? "Confirm payment?" }
|
519
|
+
|
520
|
+
# Avoid
|
521
|
+
app.screen(:input1) { |p| p.ask "Phone:" }
|
522
|
+
app.screen(:confirm) { |p| p.yes? "Confirm payment?" }
|
523
|
+
```
|
524
|
+
|
525
|
+
### 3. Validate Early and Often
|
16
526
|
|
17
|
-
|
527
|
+
Always validate user input to provide clear feedback:
|
18
528
|
|
19
|
-
|
529
|
+
```ruby
|
530
|
+
app.screen(:amount) do |prompt|
|
531
|
+
prompt.ask "Enter amount:",
|
532
|
+
convert: ->(input) { input.to_f },
|
533
|
+
validate: ->(amount) {
|
534
|
+
return "Amount must be positive" if amount <= 0
|
535
|
+
return "Maximum amount is $1000" if amount > 1000
|
536
|
+
nil
|
537
|
+
}
|
538
|
+
end
|
539
|
+
```
|
540
|
+
|
541
|
+
### 4. Handle Edge Cases
|
542
|
+
|
543
|
+
Consider error scenarios and provide helpful messages:
|
544
|
+
|
545
|
+
```ruby
|
546
|
+
def main_page
|
547
|
+
begin
|
548
|
+
process_user_request
|
549
|
+
rescue PaymentError => e
|
550
|
+
app.say "Payment failed: #{e.message}. Please try again."
|
551
|
+
rescue SystemError
|
552
|
+
app.say "System temporarily unavailable. Please try again later."
|
553
|
+
end
|
554
|
+
end
|
555
|
+
```
|
556
|
+
|
557
|
+
## Configuration
|
558
|
+
|
559
|
+
### Session Storage Options
|
560
|
+
|
561
|
+
Configure different session storage backends:
|
562
|
+
|
563
|
+
```ruby
|
564
|
+
# Rails session (default)
|
565
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
566
|
+
|
567
|
+
# Custom session store
|
568
|
+
class MySessionStore
|
569
|
+
def initialize(context)
|
570
|
+
@context = context
|
571
|
+
end
|
20
572
|
|
21
|
-
|
573
|
+
def get(key)
|
574
|
+
# Your storage logic
|
575
|
+
end
|
22
576
|
|
23
|
-
|
577
|
+
def set(key, value)
|
578
|
+
# Your storage logic
|
579
|
+
end
|
580
|
+
end
|
24
581
|
|
25
|
-
|
582
|
+
config.use_session_store MySessionStore
|
583
|
+
```
|
26
584
|
|
27
585
|
## Development
|
28
586
|
|
29
|
-
|
587
|
+
### Running Tests
|
30
588
|
|
31
|
-
|
589
|
+
FlowChat uses Minitest for testing:
|
32
590
|
|
33
|
-
|
591
|
+
```bash
|
592
|
+
# Run all tests
|
593
|
+
bundle exec rake test
|
594
|
+
|
595
|
+
# Run specific test file
|
596
|
+
bundle exec rake test TEST=test/unit/flow_test.rb
|
597
|
+
|
598
|
+
# Run specific test
|
599
|
+
bundle exec rake test TESTOPTS="--name=test_flow_initialization"
|
600
|
+
```
|
34
601
|
|
35
|
-
|
602
|
+
### Contributing
|
603
|
+
|
604
|
+
1. Fork the repository
|
605
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
606
|
+
3. Add tests for your changes
|
607
|
+
4. Ensure all tests pass (`bundle exec rake test`)
|
608
|
+
5. Commit your changes (`git commit -am 'Add amazing feature'`)
|
609
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
610
|
+
7. Open a Pull Request
|
611
|
+
|
612
|
+
## Roadmap
|
613
|
+
|
614
|
+
- ๐ฑ **WhatsApp Integration** - Support for WhatsApp Business API
|
615
|
+
- ๐ฌ **Telegram Bot Support** - Native Telegram bot integration
|
616
|
+
- ๐ **Sub-flows** - Reusable conversation components
|
617
|
+
- ๐ **Analytics Integration** - Built-in conversation analytics
|
618
|
+
- ๐ **Multi-language Support** - Internationalization features
|
619
|
+
- โก **Performance Optimizations** - Improved middleware performance
|
36
620
|
|
37
621
|
## License
|
38
622
|
|
39
|
-
|
623
|
+
FlowChat is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
624
|
+
|
625
|
+
## Support
|
626
|
+
|
627
|
+
- ๐ **Documentation**: [GitHub Repository](https://github.com/radioactive-labs/flow_chat)
|
628
|
+
- ๐ **Bug Reports**: [GitHub Issues](https://github.com/radioactive-labs/flow_chat/issues)
|
629
|
+
- ๐ฌ **Community**: Join our discussions for help and feature requests
|
data/Rakefile
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
require "
|
2
|
+
require "rake/testtask"
|
3
3
|
|
4
|
-
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
end
|
5
9
|
|
6
|
-
task default: :
|
10
|
+
task default: :test
|
data/flow_chat.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Stefan Froelich"]
|
7
7
|
spec.email = ["sfroelich01@gmail.com"]
|
8
8
|
|
9
|
-
spec.summary = "Framework for
|
10
|
-
spec.description = "Framework for
|
9
|
+
spec.summary = "Framework for building Menu based conversations (e.g. USSD) in Rails."
|
10
|
+
spec.description = "Framework for building Menu based conversations (e.g. USSD) in Rails."
|
11
11
|
spec.homepage = "https://github.com/radioactive-labs/flow_chat"
|
12
12
|
spec.license = "MIT"
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
Binary file
|
data/lib/flow_chat/config.rb
CHANGED
@@ -1,16 +1,29 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Config
|
3
|
+
# General framework configuration
|
3
4
|
mattr_accessor :logger, default: Logger.new($stdout)
|
4
5
|
mattr_accessor :cache, default: nil
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
mattr_accessor :pagination_next_text, default: "More"
|
7
|
+
# USSD-specific configuration object
|
8
|
+
def self.ussd
|
9
|
+
@ussd ||= UssdConfig.new
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
class UssdConfig
|
13
|
+
attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
|
14
|
+
:pagination_next_option, :pagination_next_text,
|
15
|
+
:resumable_sessions_enabled, :resumable_sessions_global, :resumable_sessions_timeout_seconds
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@pagination_page_size = 140
|
19
|
+
@pagination_back_option = "0"
|
20
|
+
@pagination_back_text = "Back"
|
21
|
+
@pagination_next_option = "#"
|
22
|
+
@pagination_next_text = "More"
|
23
|
+
@resumable_sessions_enabled = false
|
24
|
+
@resumable_sessions_global = true
|
25
|
+
@resumable_sessions_timeout_seconds = 300
|
26
|
+
end
|
27
|
+
end
|
15
28
|
end
|
16
29
|
end
|
data/lib/flow_chat/context.rb
CHANGED
@@ -14,8 +14,16 @@ module FlowChat
|
|
14
14
|
|
15
15
|
def input = @data["request.input"]
|
16
16
|
|
17
|
+
def input=(value)
|
18
|
+
@data["request.input"] = value
|
19
|
+
end
|
20
|
+
|
17
21
|
def session = @data["session"]
|
18
22
|
|
23
|
+
def session=(value)
|
24
|
+
@data["session"] = value
|
25
|
+
end
|
26
|
+
|
19
27
|
def controller = @data["controller"]
|
20
28
|
|
21
29
|
# def request = controller.request
|
data/lib/flow_chat/interrupt.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Interrupt
|
3
|
+
# standard:disable Lint/InheritException
|
3
4
|
class Base < Exception
|
4
5
|
attr_reader :prompt
|
5
6
|
|
@@ -8,6 +9,7 @@ module FlowChat
|
|
8
9
|
super
|
9
10
|
end
|
10
11
|
end
|
12
|
+
# standard:enable Lint/InheritException
|
11
13
|
|
12
14
|
class Prompt < Base
|
13
15
|
attr_reader :choices
|
data/lib/flow_chat/ussd/app.rb
CHANGED
@@ -16,7 +16,7 @@ module FlowChat
|
|
16
16
|
context["request.network"] = nil
|
17
17
|
context["request.msisdn"] = Phonelib.parse(params["MSISDN"]).e164
|
18
18
|
# context["request.type"] = params["MSGTYPE"] ? :initial : :response
|
19
|
-
context
|
19
|
+
context.input = params["USERDATA"].presence
|
20
20
|
|
21
21
|
type, prompt, choices = @app.call(context)
|
22
22
|
|
@@ -12,16 +12,14 @@ module FlowChat
|
|
12
12
|
|
13
13
|
if intercept?
|
14
14
|
type, prompt = handle_intercepted_request
|
15
|
-
[type, prompt, []]
|
16
15
|
else
|
17
16
|
@session.delete "ussd.pagination"
|
18
17
|
type, prompt, choices = @app.call(context)
|
19
18
|
|
20
19
|
prompt = FlowChat::Ussd::Renderer.new(prompt, choices).render
|
21
20
|
type, prompt = maybe_paginate(type, prompt) if prompt.present?
|
22
|
-
|
23
|
-
[type, prompt, []]
|
24
21
|
end
|
22
|
+
[type, prompt, []]
|
25
23
|
end
|
26
24
|
|
27
25
|
private
|
@@ -29,11 +27,11 @@ module FlowChat
|
|
29
27
|
def intercept?
|
30
28
|
pagination_state.present? &&
|
31
29
|
(pagination_state["type"].to_sym == :terminal ||
|
32
|
-
([Config.pagination_next_option, Config.pagination_back_option].include? @context.input))
|
30
|
+
([FlowChat::Config.ussd.pagination_next_option, FlowChat::Config.ussd.pagination_back_option].include? @context.input))
|
33
31
|
end
|
34
32
|
|
35
33
|
def handle_intercepted_request
|
36
|
-
Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
|
34
|
+
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
|
37
35
|
start, finish, has_more = calculate_offsets
|
38
36
|
type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
|
39
37
|
prompt = pagination_state["prompt"][start..finish].strip + build_pagination_options(type, has_more)
|
@@ -43,9 +41,9 @@ module FlowChat
|
|
43
41
|
end
|
44
42
|
|
45
43
|
def maybe_paginate(type, prompt)
|
46
|
-
if prompt.length > Config.pagination_page_size
|
44
|
+
if prompt.length > FlowChat::Config.ussd.pagination_page_size
|
47
45
|
original_prompt = prompt
|
48
|
-
Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{Config.pagination_page_size}). Paginating."
|
46
|
+
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{FlowChat::Config.ussd.pagination_page_size}). Paginating."
|
49
47
|
prompt = prompt[0..single_option_slice_size]
|
50
48
|
# Ensure we do not cut words and options off in the middle.
|
51
49
|
current_pagebreak = prompt[single_option_slice_size + 1].blank? ? single_option_slice_size : prompt.rindex("\n") || prompt.rindex(" ") || single_option_slice_size
|
@@ -60,12 +58,12 @@ module FlowChat
|
|
60
58
|
page = current_page
|
61
59
|
offset = pagination_state["offsets"][page.to_s]
|
62
60
|
if offset.present?
|
63
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
|
61
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
|
64
62
|
start = offset["start"]
|
65
63
|
finish = offset["finish"]
|
66
64
|
has_more = pagination_state["prompt"].length > finish
|
67
65
|
else
|
68
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
|
66
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
|
69
67
|
# We are guaranteed a previous offset because it was set in maybe_paginate
|
70
68
|
previous_page = page - 1
|
71
69
|
previous_offset = pagination_state["offsets"][previous_page.to_s]
|
@@ -73,8 +71,7 @@ module FlowChat
|
|
73
71
|
has_more, len = (pagination_state["prompt"].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
|
74
72
|
finish = start + len
|
75
73
|
if start > pagination_state["prompt"].length
|
76
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
|
77
|
-
page -= 1
|
74
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
|
78
75
|
has_more = false
|
79
76
|
start = previous_offset["start"]
|
80
77
|
finish = previous_offset["finish"]
|
@@ -100,11 +97,11 @@ module FlowChat
|
|
100
97
|
end
|
101
98
|
|
102
99
|
def next_option
|
103
|
-
"#{Config.pagination_next_option} #{Config.pagination_next_text}"
|
100
|
+
"#{FlowChat::Config.ussd.pagination_next_option} #{FlowChat::Config.ussd.pagination_next_text}"
|
104
101
|
end
|
105
102
|
|
106
103
|
def back_option
|
107
|
-
"#{Config.pagination_back_option} #{Config.pagination_back_text}"
|
104
|
+
"#{FlowChat::Config.ussd.pagination_back_option} #{FlowChat::Config.ussd.pagination_back_text}"
|
108
105
|
end
|
109
106
|
|
110
107
|
def single_option_slice_size
|
@@ -112,7 +109,7 @@ module FlowChat
|
|
112
109
|
# To display a single back or next option
|
113
110
|
# We accomodate the 2 newlines and the longest of the options
|
114
111
|
# We subtract an additional 1 to normalize it for slicing
|
115
|
-
@single_option_slice_size = Config.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
|
112
|
+
@single_option_slice_size = FlowChat::Config.ussd.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
|
116
113
|
end
|
117
114
|
@single_option_slice_size
|
118
115
|
end
|
@@ -121,16 +118,16 @@ module FlowChat
|
|
121
118
|
unless @dual_options_slice_size.present?
|
122
119
|
# To display both back and next options
|
123
120
|
# We accomodate the 3 newlines and both of the options
|
124
|
-
@dual_options_slice_size = Config.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
|
121
|
+
@dual_options_slice_size = FlowChat::Config.ussd.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
|
125
122
|
end
|
126
123
|
@dual_options_slice_size
|
127
124
|
end
|
128
125
|
|
129
126
|
def current_page
|
130
127
|
page = pagination_state["page"]
|
131
|
-
if @context.input == Config.pagination_back_option
|
128
|
+
if @context.input == FlowChat::Config.ussd.pagination_back_option
|
132
129
|
page -= 1
|
133
|
-
elsif @context.input == Config.pagination_next_option
|
130
|
+
elsif @context.input == FlowChat::Config.ussd.pagination_next_option
|
134
131
|
page += 1
|
135
132
|
end
|
136
133
|
[page, 1].max
|
@@ -7,44 +7,31 @@ module FlowChat
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(context)
|
10
|
-
if Config.resumable_sessions_enabled && context["ussd.request"].present?
|
11
|
-
|
12
|
-
session
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
context["ussd.request"][:type] = :response
|
21
|
-
context["ussd.resumable_sessions"][:resumed] = true
|
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
|
22
20
|
end
|
23
|
-
|
24
|
-
res = @app.call(context)
|
25
|
-
|
26
|
-
if context["ussd.response"].present?
|
27
|
-
if context["ussd.response"][:type] == :terminal || context["ussd.resumable_sessions"][:disable]
|
28
|
-
session.delete "ussd.resumable_sessions"
|
29
|
-
else
|
30
|
-
session["ussd.resumable_sessions"] = Time.now.to_i
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
res
|
35
|
-
else
|
36
|
-
@app.call(context)
|
37
21
|
end
|
22
|
+
|
23
|
+
@app.call(context)
|
38
24
|
end
|
39
25
|
|
40
26
|
private
|
41
27
|
|
42
|
-
def
|
43
|
-
return unless
|
44
|
-
return true unless Config.resumable_sessions_timeout_seconds
|
28
|
+
def valid?(session)
|
29
|
+
return true unless FlowChat::Config.ussd.resumable_sessions_timeout_seconds
|
45
30
|
|
46
|
-
last_active_at = Time.
|
47
|
-
(Time.now - Config.resumable_sessions_timeout_seconds) < last_active_at
|
31
|
+
last_active_at = Time.parse session.dig("context", "last_active_at")
|
32
|
+
(Time.now - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
|
33
|
+
rescue
|
34
|
+
false
|
48
35
|
end
|
49
36
|
end
|
50
37
|
end
|
@@ -32,7 +32,7 @@ module FlowChat
|
|
32
32
|
msg,
|
33
33
|
choices: choices_prompt,
|
34
34
|
convert: lambda { |choice| choice.to_i },
|
35
|
-
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).
|
35
|
+
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).cover?(choice) },
|
36
36
|
transform: lambda { |choice| choices[choice - 1] }
|
37
37
|
)
|
38
38
|
end
|
@@ -40,22 +40,22 @@
|
|
40
40
|
</head>
|
41
41
|
<body>
|
42
42
|
<div class="content">
|
43
|
-
<div class="field
|
43
|
+
<div class="field <%= show_options ? '' : 'hidden' %>">
|
44
44
|
<div class="label">Provider </div>
|
45
45
|
<div class="value">
|
46
46
|
<select id="provider">
|
47
|
-
<option <%=
|
48
|
-
<option <%=
|
47
|
+
<option <%= default_provider == :nalo ? 'selected' : '' %> value="nalo">Nalo</option>
|
48
|
+
<option <%= default_provider == :nsano ? 'selected' : '' %> value="nsano">Nsano</option>
|
49
49
|
</select>
|
50
50
|
</div>
|
51
51
|
</div>
|
52
|
-
<div class="field <%=
|
52
|
+
<div class="field <%= show_options ? '' : 'hidden' %>">
|
53
53
|
<div class="label">Endpoint </div>
|
54
54
|
<div class="value">
|
55
55
|
<input id="endpoint" value="<%= default_endpoint %>" />
|
56
56
|
</div>
|
57
57
|
</div>
|
58
|
-
<div class="field <%=
|
58
|
+
<div class="field <%= show_options ? '' : 'hidden' %>">
|
59
59
|
<div class="label">MSISDN </div>
|
60
60
|
<div class="value">
|
61
61
|
<input id="msisdn" value="<%= default_msisdn %>" />
|
data/lib/flow_chat/version.rb
CHANGED
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.3.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:
|
11
|
+
date: 2025-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -80,15 +80,16 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.4.2
|
83
|
-
description: Framework for
|
83
|
+
description: Framework for building Menu based conversations (e.g. USSD) in Rails.
|
84
84
|
email:
|
85
85
|
- sfroelich01@gmail.com
|
86
86
|
executables: []
|
87
87
|
extensions: []
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
|
+
- ".DS_Store"
|
90
91
|
- ".gitignore"
|
91
|
-
- ".
|
92
|
+
- ".ruby-version"
|
92
93
|
- ".travis.yml"
|
93
94
|
- Gemfile
|
94
95
|
- LICENSE.txt
|
@@ -97,9 +98,11 @@ files:
|
|
97
98
|
- bin/console
|
98
99
|
- bin/setup
|
99
100
|
- flow_chat.gemspec
|
101
|
+
- images/ussd_simulator.png
|
100
102
|
- lib/flow_chat.rb
|
101
103
|
- lib/flow_chat/config.rb
|
102
104
|
- lib/flow_chat/context.rb
|
105
|
+
- lib/flow_chat/flow.rb
|
103
106
|
- lib/flow_chat/interrupt.rb
|
104
107
|
- lib/flow_chat/session/middleware.rb
|
105
108
|
- lib/flow_chat/session/rails_session_store.rb
|
@@ -138,8 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
141
|
- !ruby/object:Gem::Version
|
139
142
|
version: '0'
|
140
143
|
requirements: []
|
141
|
-
rubygems_version: 3.
|
144
|
+
rubygems_version: 3.4.10
|
142
145
|
signing_key:
|
143
146
|
specification_version: 4
|
144
|
-
summary: Framework for
|
147
|
+
summary: Framework for building Menu based conversations (e.g. USSD) in Rails.
|
145
148
|
test_files: []
|
data/.rspec
DELETED