flow_chat 0.3.0 → 0.4.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 +266 -16
- data/examples/initializer.rb +31 -0
- data/examples/multi_tenant_whatsapp_controller.rb +248 -0
- data/examples/ussd_controller.rb +264 -0
- data/examples/whatsapp_controller.rb +141 -0
- data/lib/flow_chat/base_processor.rb +63 -0
- data/lib/flow_chat/session/cache_session_store.rb +84 -0
- data/lib/flow_chat/session/middleware.rb +14 -6
- data/lib/flow_chat/ussd/app.rb +25 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +2 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +6 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +15 -42
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +58 -0
- data/lib/flow_chat/whatsapp/configuration.rb +75 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
- data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
- data/lib/flow_chat/whatsapp/processor.rb +36 -0
- data/lib/flow_chat/whatsapp/prompt.rb +206 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
- data/lib/flow_chat.rb +1 -0
- metadata +14 -1
@@ -0,0 +1,264 @@
|
|
1
|
+
# Example USSD Controller
|
2
|
+
# Add this to your Rails application as app/controllers/ussd_controller.rb
|
3
|
+
|
4
|
+
class UssdController < ApplicationController
|
5
|
+
skip_forgery_protection
|
6
|
+
|
7
|
+
def process_request
|
8
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
9
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
10
|
+
# Use Rails session for USSD (shorter sessions)
|
11
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
12
|
+
|
13
|
+
# Enable resumable sessions (optional)
|
14
|
+
config.use_resumable_sessions
|
15
|
+
end
|
16
|
+
|
17
|
+
processor.run WelcomeFlow, :main_page
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Example Flow for USSD
|
22
|
+
# Add this to your Rails application as app/flow_chat/welcome_flow.rb
|
23
|
+
|
24
|
+
class WelcomeFlow < FlowChat::Flow
|
25
|
+
def main_page
|
26
|
+
# Welcome the user
|
27
|
+
name = app.screen(:name) do |prompt|
|
28
|
+
prompt.ask "Welcome to our service! What's your name?",
|
29
|
+
transform: ->(input) { input.strip.titleize }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Show main menu with numbered options (USSD style)
|
33
|
+
choice = app.screen(:main_menu) do |prompt|
|
34
|
+
prompt.select "Hi #{name}! Choose an option:", {
|
35
|
+
"1" => "Account Info",
|
36
|
+
"2" => "Make Payment",
|
37
|
+
"3" => "Get Balance",
|
38
|
+
"4" => "Customer Support"
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
case choice
|
43
|
+
when "1"
|
44
|
+
show_account_info
|
45
|
+
when "2"
|
46
|
+
make_payment
|
47
|
+
when "3"
|
48
|
+
get_balance
|
49
|
+
when "4"
|
50
|
+
customer_support
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def show_account_info
|
57
|
+
info_choice = app.screen(:account_info) do |prompt|
|
58
|
+
prompt.select "Account Information:", {
|
59
|
+
"1" => "Personal Details",
|
60
|
+
"2" => "Account Balance",
|
61
|
+
"3" => "Transaction History",
|
62
|
+
"0" => "Back to Main Menu"
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
case info_choice
|
67
|
+
when "1"
|
68
|
+
app.say "Name: John Doe\\nPhone: #{app.phone_number}\\nAccount: Active"
|
69
|
+
when "2"
|
70
|
+
app.say "Current Balance: $150.75\\nAvailable Credit: $1,000.00"
|
71
|
+
when "3"
|
72
|
+
app.say "Last 3 Transactions:\\n1. +$50.00 - Deposit\\n2. -$25.50 - Purchase\\n3. -$15.00 - Transfer"
|
73
|
+
when "0"
|
74
|
+
main_page # Go back to main menu
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def make_payment
|
79
|
+
amount = app.screen(:payment_amount) do |prompt|
|
80
|
+
prompt.ask "Enter amount to pay:",
|
81
|
+
convert: ->(input) { input.to_f },
|
82
|
+
validate: ->(amount) {
|
83
|
+
return "Amount must be greater than 0" unless amount > 0
|
84
|
+
return "Maximum payment is $500" unless amount <= 500
|
85
|
+
nil
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
recipient = app.screen(:payment_recipient) do |prompt|
|
90
|
+
prompt.ask "Enter recipient phone number:",
|
91
|
+
validate: ->(input) {
|
92
|
+
return "Phone number must be 10 digits" unless input.match?(/\\A\\d{10}\\z/)
|
93
|
+
nil
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Confirmation screen
|
98
|
+
confirmed = app.screen(:payment_confirmation) do |prompt|
|
99
|
+
prompt.yes? "Pay $#{amount} to #{recipient}?\\nConfirm payment?"
|
100
|
+
end
|
101
|
+
|
102
|
+
if confirmed
|
103
|
+
# Process payment (your business logic here)
|
104
|
+
transaction_id = process_payment(amount, recipient)
|
105
|
+
app.say "Payment successful!\\nTransaction ID: #{transaction_id}\\nAmount: $#{amount}\\nTo: #{recipient}"
|
106
|
+
else
|
107
|
+
app.say "Payment cancelled"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_balance
|
112
|
+
# Simulate balance check
|
113
|
+
balance = check_account_balance(app.phone_number)
|
114
|
+
app.say "Account Balance\\n\\nAvailable: $#{balance[:available]}\\nPending: $#{balance[:pending]}\\nTotal: $#{balance[:total]}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def customer_support
|
118
|
+
support_choice = app.screen(:support_menu) do |prompt|
|
119
|
+
prompt.select "Customer Support:", {
|
120
|
+
"1" => "Report an Issue",
|
121
|
+
"2" => "Account Questions",
|
122
|
+
"3" => "Technical Support",
|
123
|
+
"4" => "Speak to Agent",
|
124
|
+
"0" => "Main Menu"
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
case support_choice
|
129
|
+
when "1"
|
130
|
+
report_issue
|
131
|
+
when "2"
|
132
|
+
app.say "For account questions:\\nCall: 123-456-7890\\nEmail: support@company.com\\nHours: 9AM-5PM Mon-Fri"
|
133
|
+
when "3"
|
134
|
+
app.say "Technical Support:\\nCall: 123-456-7891\\nEmail: tech@company.com\\n24/7 Support Available"
|
135
|
+
when "4"
|
136
|
+
app.say "Connecting you to an agent...\\nPlease call 123-456-7890\\nOr visit our nearest branch"
|
137
|
+
when "0"
|
138
|
+
main_page
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def report_issue
|
143
|
+
issue_type = app.screen(:issue_type) do |prompt|
|
144
|
+
prompt.select "Select issue type:", {
|
145
|
+
"1" => "Payment Problem",
|
146
|
+
"2" => "Account Access",
|
147
|
+
"3" => "Service Error",
|
148
|
+
"4" => "Other"
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
description = app.screen(:issue_description) do |prompt|
|
153
|
+
prompt.ask "Briefly describe the issue:",
|
154
|
+
validate: ->(input) {
|
155
|
+
return "Description must be at least 10 characters" unless input.length >= 10
|
156
|
+
nil
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
# Save the issue (your business logic here)
|
161
|
+
ticket_id = create_support_ticket(issue_type, description, app.phone_number)
|
162
|
+
|
163
|
+
app.say "Issue reported successfully!\\n\\nTicket ID: #{ticket_id}\\nWe'll contact you within 24 hours.\\n\\nThank you!"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Helper methods (implement your business logic)
|
167
|
+
|
168
|
+
def process_payment(amount, recipient)
|
169
|
+
# Your payment processing logic here
|
170
|
+
# Return transaction ID
|
171
|
+
"TXN#{rand(100000..999999)}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def check_account_balance(phone_number)
|
175
|
+
# Your balance checking logic here
|
176
|
+
{
|
177
|
+
available: "150.75",
|
178
|
+
pending: "25.00",
|
179
|
+
total: "175.75"
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_support_ticket(issue_type, description, phone_number)
|
184
|
+
# Your ticket creation logic here
|
185
|
+
Rails.logger.info "Support ticket created: #{issue_type} - #{description} from #{phone_number}"
|
186
|
+
"TICKET#{rand(10000..99999)}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Configuration Examples:
|
191
|
+
|
192
|
+
# 1. Basic configuration with custom pagination
|
193
|
+
class UssdController < ApplicationController
|
194
|
+
skip_forgery_protection
|
195
|
+
|
196
|
+
def process_request
|
197
|
+
# Configure pagination for shorter messages
|
198
|
+
FlowChat::Config.ussd.pagination_page_size = 120
|
199
|
+
FlowChat::Config.ussd.pagination_next_option = "#"
|
200
|
+
FlowChat::Config.ussd.pagination_back_option = "*"
|
201
|
+
|
202
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
203
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
204
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
205
|
+
end
|
206
|
+
|
207
|
+
processor.run WelcomeFlow, :main_page
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# 2. Configuration with custom middleware
|
212
|
+
class LoggingMiddleware
|
213
|
+
def initialize(app)
|
214
|
+
@app = app
|
215
|
+
end
|
216
|
+
|
217
|
+
def call(context)
|
218
|
+
Rails.logger.info "USSD Request from #{context['request.msisdn']}: #{context.input}"
|
219
|
+
start_time = Time.current
|
220
|
+
|
221
|
+
result = @app.call(context)
|
222
|
+
|
223
|
+
duration = Time.current - start_time
|
224
|
+
Rails.logger.info "USSD Response (#{duration.round(3)}s): #{result[1]}"
|
225
|
+
|
226
|
+
result
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
class UssdController < ApplicationController
|
231
|
+
skip_forgery_protection
|
232
|
+
|
233
|
+
def process_request
|
234
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
235
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
236
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
237
|
+
config.use_middleware LoggingMiddleware # Add custom logging
|
238
|
+
config.use_resumable_sessions # Enable resumable sessions
|
239
|
+
end
|
240
|
+
|
241
|
+
processor.run WelcomeFlow, :main_page
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# 3. Configuration with cache-based sessions for longer persistence
|
246
|
+
class UssdController < ApplicationController
|
247
|
+
skip_forgery_protection
|
248
|
+
|
249
|
+
def process_request
|
250
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
251
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
252
|
+
# Use cache store for longer session persistence
|
253
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
254
|
+
end
|
255
|
+
|
256
|
+
processor.run WelcomeFlow, :main_page
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Add this route to your config/routes.rb:
|
261
|
+
# post '/ussd', to: 'ussd#process_request'
|
262
|
+
|
263
|
+
# For Nsano gateway, use:
|
264
|
+
# config.use_gateway FlowChat::Ussd::Gateway::Nsano
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Example WhatsApp Controller
|
2
|
+
# Add this to your Rails application as app/controllers/whatsapp_controller.rb
|
3
|
+
|
4
|
+
class WhatsappController < ApplicationController
|
5
|
+
skip_forgery_protection
|
6
|
+
|
7
|
+
def webhook
|
8
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
9
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
10
|
+
# Use cache-based session store for longer WhatsApp conversations
|
11
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
12
|
+
end
|
13
|
+
|
14
|
+
processor.run WelcomeFlow, :main_page
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Example with Custom Configuration
|
19
|
+
class CustomWhatsappController < ApplicationController
|
20
|
+
skip_forgery_protection
|
21
|
+
|
22
|
+
def webhook
|
23
|
+
# Create custom WhatsApp configuration for this endpoint
|
24
|
+
custom_config = FlowChat::Whatsapp::Configuration.new
|
25
|
+
custom_config.access_token = ENV['MY_WHATSAPP_ACCESS_TOKEN']
|
26
|
+
custom_config.phone_number_id = ENV['MY_WHATSAPP_PHONE_NUMBER_ID']
|
27
|
+
custom_config.verify_token = ENV['MY_WHATSAPP_VERIFY_TOKEN']
|
28
|
+
custom_config.app_id = ENV['MY_WHATSAPP_APP_ID']
|
29
|
+
custom_config.app_secret = ENV['MY_WHATSAPP_APP_SECRET']
|
30
|
+
custom_config.business_account_id = ENV['MY_WHATSAPP_BUSINESS_ACCOUNT_ID']
|
31
|
+
|
32
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
33
|
+
config.use_whatsapp_config(custom_config) # Use custom config
|
34
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
35
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
36
|
+
end
|
37
|
+
|
38
|
+
processor.run WelcomeFlow, :main_page
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Example Flow for WhatsApp
|
43
|
+
# Add this to your Rails application as app/flow_chat/welcome_flow.rb
|
44
|
+
|
45
|
+
class WelcomeFlow < FlowChat::Flow
|
46
|
+
def main_page
|
47
|
+
# Welcome the user
|
48
|
+
name = app.screen(:name) do |prompt|
|
49
|
+
prompt.ask "Hello! Welcome to our WhatsApp service. What's your name?",
|
50
|
+
transform: ->(input) { input.strip.titleize }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Show main menu
|
54
|
+
choice = app.screen(:main_menu) do |prompt|
|
55
|
+
prompt.select "Hi #{name}! What can I help you with today?", {
|
56
|
+
"info" => "📋 Get Information",
|
57
|
+
"support" => "🆘 Contact Support",
|
58
|
+
"feedback" => "💬 Give Feedback"
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
case choice
|
63
|
+
when "info"
|
64
|
+
show_information_menu
|
65
|
+
when "support"
|
66
|
+
contact_support
|
67
|
+
when "feedback"
|
68
|
+
collect_feedback
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def show_information_menu
|
75
|
+
info_choice = app.screen(:info_menu) do |prompt|
|
76
|
+
prompt.select "What information do you need?", {
|
77
|
+
"hours" => "🕒 Business Hours",
|
78
|
+
"location" => "📍 Our Location",
|
79
|
+
"services" => "🛠 Our Services"
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
case info_choice
|
84
|
+
when "hours"
|
85
|
+
app.say "We're open Monday-Friday 9AM-6PM, Saturday 9AM-2PM. Closed Sundays."
|
86
|
+
when "location"
|
87
|
+
app.say "📍 We're located at 123 Main Street, City, State 12345"
|
88
|
+
when "services"
|
89
|
+
app.say "Here are our main services:\\n\\n🌐 Web Development - Custom websites and applications\\n📱 Mobile Apps - iOS and Android development\\n🔧 Consulting - Technical consulting services"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def contact_support
|
94
|
+
# Use standard select menu instead of send_buttons
|
95
|
+
contact_method = app.screen(:contact_method) do |prompt|
|
96
|
+
prompt.select "How would you like to contact support?", {
|
97
|
+
"call" => "📞 Call Us",
|
98
|
+
"email" => "📧 Email Us",
|
99
|
+
"chat" => "💬 Live Chat"
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
case contact_method
|
104
|
+
when "call"
|
105
|
+
app.say "📞 You can call us at (555) 123-4567"
|
106
|
+
when "email"
|
107
|
+
app.say "📧 Send us an email at support@example.com"
|
108
|
+
when "chat"
|
109
|
+
app.say "💬 Our live chat is available on our website: www.example.com"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def collect_feedback
|
114
|
+
rating = app.screen(:rating) do |prompt|
|
115
|
+
prompt.select "How would you rate our service?", {
|
116
|
+
"5" => "⭐⭐⭐⭐⭐ Excellent",
|
117
|
+
"4" => "⭐⭐⭐⭐ Good",
|
118
|
+
"3" => "⭐⭐⭐ Average",
|
119
|
+
"2" => "⭐⭐ Poor",
|
120
|
+
"1" => "⭐ Very Poor"
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
feedback = app.screen(:feedback_text) do |prompt|
|
125
|
+
prompt.ask "Thank you for the #{rating}-star rating! Please share any additional feedback:"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Save feedback (implement your logic here)
|
129
|
+
save_feedback(app.phone_number, rating, feedback)
|
130
|
+
|
131
|
+
app.say "Thank you for your feedback! We really appreciate it. 🙏"
|
132
|
+
end
|
133
|
+
|
134
|
+
def save_feedback(phone, rating, feedback)
|
135
|
+
# Implement your feedback saving logic here
|
136
|
+
Rails.logger.info "Feedback from #{phone}: #{rating} stars - #{feedback}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Add this route to your config/routes.rb:
|
141
|
+
# post '/whatsapp/webhook', to: 'whatsapp#webhook'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
module FlowChat
|
4
|
+
class BaseProcessor
|
5
|
+
attr_reader :middleware, :gateway
|
6
|
+
|
7
|
+
def initialize(controller)
|
8
|
+
@context = FlowChat::Context.new
|
9
|
+
@context["controller"] = controller
|
10
|
+
@middleware = ::Middleware::Builder.new(name: middleware_name)
|
11
|
+
|
12
|
+
yield self if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def use_gateway(gateway)
|
16
|
+
@gateway = gateway
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def use_session_store(session_store)
|
21
|
+
@context["session.store"] = session_store
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_middleware(middleware)
|
26
|
+
@middleware.use middleware
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def run(flow_class, action)
|
31
|
+
@context["flow.name"] = flow_class.name.underscore
|
32
|
+
@context["flow.class"] = flow_class
|
33
|
+
@context["flow.action"] = action
|
34
|
+
|
35
|
+
stack = build_middleware_stack
|
36
|
+
yield stack if block_given?
|
37
|
+
|
38
|
+
stack.call(@context)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Subclasses should override these methods
|
44
|
+
def middleware_name
|
45
|
+
raise NotImplementedError, "Subclasses must implement middleware_name"
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_middleware_stack
|
49
|
+
raise NotImplementedError, "Subclasses must implement build_middleware_stack"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Helper method for building stacks
|
53
|
+
def create_middleware_stack(name)
|
54
|
+
::Middleware::Builder.new(name: name) do |b|
|
55
|
+
configure_middleware_stack(b)
|
56
|
+
end.inject_logger(Rails.logger)
|
57
|
+
end
|
58
|
+
|
59
|
+
def configure_middleware_stack(builder)
|
60
|
+
raise NotImplementedError, "Subclasses must implement configure_middleware_stack"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Session
|
3
|
+
class CacheSessionStore
|
4
|
+
def initialize(context, cache = nil)
|
5
|
+
@context = context
|
6
|
+
@cache = cache || FlowChat::Config.cache
|
7
|
+
|
8
|
+
raise ArgumentError, "Cache is required. Set FlowChat::Config.cache or pass a cache instance." unless @cache
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(key)
|
12
|
+
return nil unless @context
|
13
|
+
|
14
|
+
data = @cache.read(session_key)
|
15
|
+
return nil unless data
|
16
|
+
|
17
|
+
data[key.to_s]
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(key, value)
|
21
|
+
return unless @context
|
22
|
+
|
23
|
+
data = @cache.read(session_key) || {}
|
24
|
+
data[key.to_s] = value
|
25
|
+
|
26
|
+
@cache.write(session_key, data, expires_in: session_ttl)
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(key)
|
31
|
+
return unless @context
|
32
|
+
|
33
|
+
data = @cache.read(session_key)
|
34
|
+
return unless data
|
35
|
+
|
36
|
+
data.delete(key.to_s)
|
37
|
+
@cache.write(session_key, data, expires_in: session_ttl)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
return unless @context
|
42
|
+
|
43
|
+
@cache.delete(session_key)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Alias for compatibility
|
47
|
+
alias_method :destroy, :clear
|
48
|
+
|
49
|
+
def exists?
|
50
|
+
@cache.exist?(session_key)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def session_key
|
56
|
+
gateway = @context["request.gateway"]
|
57
|
+
msisdn = @context["request.msisdn"]
|
58
|
+
|
59
|
+
case gateway
|
60
|
+
when :whatsapp_cloud_api
|
61
|
+
"flow_chat:session:whatsapp:#{msisdn}"
|
62
|
+
when :nalo, :nsano
|
63
|
+
session_id = @context["request.id"]
|
64
|
+
"flow_chat:session:ussd:#{session_id}:#{msisdn}"
|
65
|
+
else
|
66
|
+
"flow_chat:session:unknown:#{msisdn}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def session_ttl
|
71
|
+
gateway = @context["request.gateway"]
|
72
|
+
|
73
|
+
case gateway
|
74
|
+
when :whatsapp_cloud_api
|
75
|
+
7.days # WhatsApp conversations can be long-lived
|
76
|
+
when :nalo, :nsano
|
77
|
+
1.hour # USSD sessions are typically short
|
78
|
+
else
|
79
|
+
1.day # Default
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -14,12 +14,20 @@ module FlowChat
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def session_id(context)
|
17
|
-
context["request.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
gateway = context["request.gateway"]
|
18
|
+
flow_name = context["flow.name"]
|
19
|
+
case gateway
|
20
|
+
when :whatsapp_cloud_api
|
21
|
+
# For WhatsApp, use phone number + flow name for consistent sessions
|
22
|
+
phone = context["request.msisdn"]
|
23
|
+
"#{gateway}:#{flow_name}:#{phone}"
|
24
|
+
# when :nalo, :nsano
|
25
|
+
# # For USSD, use the request ID from the gateway
|
26
|
+
# "#{gateway}:#{flow_name}:#{context["request.id"]}"
|
27
|
+
else
|
28
|
+
# Fallback to request ID
|
29
|
+
"#{gateway}:#{flow_name}:#{context["request.id"]}"
|
30
|
+
end
|
23
31
|
end
|
24
32
|
end
|
25
33
|
end
|
data/lib/flow_chat/ussd/app.rb
CHANGED
@@ -28,6 +28,31 @@ module FlowChat
|
|
28
28
|
def say(msg)
|
29
29
|
raise FlowChat::Interrupt::Terminate.new(msg)
|
30
30
|
end
|
31
|
+
|
32
|
+
# WhatsApp-specific data accessors (not supported in USSD)
|
33
|
+
def contact_name
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def message_id
|
38
|
+
context["request.message_id"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def timestamp
|
42
|
+
context["request.timestamp"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def location
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def media
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def phone_number
|
54
|
+
context["request.msisdn"]
|
55
|
+
end
|
31
56
|
end
|
32
57
|
end
|
33
58
|
end
|
@@ -12,6 +12,8 @@ module FlowChat
|
|
12
12
|
params = context.controller.request.params
|
13
13
|
|
14
14
|
context["request.id"] = params["USERID"]
|
15
|
+
context["request.message_id"] = SecureRandom.uuid
|
16
|
+
context["request.timestamp"] = Time.current.iso8601
|
15
17
|
context["request.gateway"] = :nalo
|
16
18
|
context["request.network"] = nil
|
17
19
|
context["request.msisdn"] = Phonelib.parse(params["MSISDN"]).e164
|
@@ -12,6 +12,12 @@ module FlowChat
|
|
12
12
|
controller = context["controller"]
|
13
13
|
controller.request
|
14
14
|
|
15
|
+
# Add timestamp for all requests
|
16
|
+
context["request.timestamp"] = Time.current.iso8601
|
17
|
+
|
18
|
+
# Set a basic message_id (can be enhanced based on actual Nsano implementation)
|
19
|
+
context["request.message_id"] = SecureRandom.uuid
|
20
|
+
|
15
21
|
# input = context["rack.input"].read
|
16
22
|
# context["rack.input"].rewind
|
17
23
|
# if input.present?
|
@@ -29,7 +29,7 @@ module FlowChat
|
|
29
29
|
return true unless FlowChat::Config.ussd.resumable_sessions_timeout_seconds
|
30
30
|
|
31
31
|
last_active_at = Time.parse session.dig("context", "last_active_at")
|
32
|
-
(Time.
|
32
|
+
(Time.current - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
|
33
33
|
rescue
|
34
34
|
false
|
35
35
|
end
|