flow_chat 0.8.2 → 0.10.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/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/pages.yml +43 -0
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +189 -133
- data/Rakefile +17 -2
- data/SECURITY.md +42 -349
- data/docs/architecture.md +83 -0
- data/docs/async-background-processing.md +64 -0
- data/docs/configuration.md +110 -287
- data/docs/factory-pattern.md +58 -0
- data/docs/gateway-context-variables.md +168 -0
- data/docs/gateway-development.md +159 -0
- data/docs/getting-started.md +90 -0
- data/docs/instrumentation.md +95 -175
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +109 -0
- data/docs/platforms/ussd.md +78 -0
- data/docs/platforms/whatsapp.md +147 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +33 -426
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +22 -20
- data/examples/intercom_configuration_example.rb +113 -0
- data/examples/intercom_controller.rb +182 -0
- data/examples/multi_tenant_whatsapp_controller.rb +63 -168
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +88 -160
- data/examples/whatsapp_controller.rb +18 -17
- data/examples/whatsapp_media_examples.rb +27 -79
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/app.rb +211 -0
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +126 -23
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +104 -36
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/http/renderer.rb +3 -3
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +182 -0
- data/lib/flow_chat/intercom/client.rb +161 -0
- data/lib/flow_chat/intercom/configuration.rb +102 -0
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +123 -0
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/phone_number_util.rb +37 -35
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +167 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +36 -11
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +283 -0
- data/lib/flow_chat/telegram/configuration.rb +78 -0
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
- data/lib/flow_chat/telegram/renderer.rb +133 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +158 -20
- data/lib/flow_chat/whatsapp/configuration.rb +13 -52
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
- data/lib/flow_chat/whatsapp/renderer.rb +259 -64
- data/lib/flow_chat.rb +1 -1
- data/lib/tasks/release.rake +165 -0
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +132 -25
- data/docs/flows.md +0 -320
- data/docs/http-gateway-protocol.md +0 -432
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_app.rb +0 -86
- data/lib/flow_chat/base_processor.rb +0 -146
- data/lib/flow_chat/http/app.rb +0 -6
- data/lib/flow_chat/http/middleware/executor.rb +0 -24
- data/lib/flow_chat/http/processor.rb +0 -33
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
|
@@ -1,220 +1,115 @@
|
|
|
1
1
|
# Example Multi-Tenant WhatsApp Controller
|
|
2
|
-
#
|
|
2
|
+
# Shows how to run different WhatsApp accounts per tenant on one endpoint.
|
|
3
|
+
|
|
4
|
+
# A single helper builds a Configuration from a set of environment variables,
|
|
5
|
+
# so each tenant is one line instead of a repeated block. Pass an anonymous
|
|
6
|
+
# configuration name (nil); Configuration#initialize requires a name argument.
|
|
7
|
+
module WhatsappConfigBuilder
|
|
8
|
+
def whatsapp_config_from_env(prefix)
|
|
9
|
+
FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
|
|
10
|
+
config.access_token = ENV["#{prefix}_WHATSAPP_ACCESS_TOKEN"]
|
|
11
|
+
config.phone_number_id = ENV["#{prefix}_WHATSAPP_PHONE_NUMBER_ID"]
|
|
12
|
+
config.verify_token = ENV["#{prefix}_WHATSAPP_VERIFY_TOKEN"]
|
|
13
|
+
config.app_secret = ENV["#{prefix}_WHATSAPP_APP_SECRET"]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
3
17
|
|
|
4
|
-
# Controller supporting multiple WhatsApp accounts per tenant
|
|
18
|
+
# Controller supporting multiple WhatsApp accounts per tenant.
|
|
5
19
|
class MultiTenantWhatsappController < ApplicationController
|
|
20
|
+
include WhatsappConfigBuilder
|
|
21
|
+
|
|
6
22
|
skip_forgery_protection
|
|
7
23
|
|
|
8
24
|
def webhook
|
|
9
25
|
tenant = determine_tenant(request)
|
|
10
|
-
whatsapp_config =
|
|
26
|
+
whatsapp_config = config_for_tenant(tenant)
|
|
11
27
|
|
|
12
|
-
processor = FlowChat::
|
|
28
|
+
processor = FlowChat::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
|
|
13
29
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
14
30
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
15
31
|
end
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
processor.run flow_class, :main_page
|
|
33
|
+
processor.run flow_for_tenant(tenant), :main_page
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
private
|
|
22
37
|
|
|
23
38
|
def determine_tenant(request)
|
|
24
|
-
# Option 1: From subdomain
|
|
25
39
|
return request.subdomain if request.subdomain.present?
|
|
26
40
|
|
|
27
|
-
# Option 2: From path (e.g., /whatsapp/acme/webhook)
|
|
28
41
|
tenant_from_path = request.path.match(%r{^/whatsapp/(\w+)/})&.captures&.first
|
|
29
42
|
return tenant_from_path if tenant_from_path
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
return request.headers["X-Tenant-ID"] if request.headers["X-Tenant-ID"]
|
|
33
|
-
|
|
34
|
-
"default"
|
|
44
|
+
request.headers["X-Tenant-ID"].presence || "default"
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
def
|
|
47
|
+
def config_for_tenant(tenant)
|
|
38
48
|
case tenant
|
|
39
|
-
when "acme_corp"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
config.verify_token = ENV["ACME_WHATSAPP_VERIFY_TOKEN"]
|
|
44
|
-
config.app_secret = ENV["ACME_WHATSAPP_APP_SECRET"]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
when "tech_startup"
|
|
48
|
-
FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
49
|
-
config.access_token = ENV["TECHSTARTUP_WHATSAPP_ACCESS_TOKEN"]
|
|
50
|
-
config.phone_number_id = ENV["TECHSTARTUP_WHATSAPP_PHONE_NUMBER_ID"]
|
|
51
|
-
config.verify_token = ENV["TECHSTARTUP_WHATSAPP_VERIFY_TOKEN"]
|
|
52
|
-
config.app_secret = ENV["TECHSTARTUP_WHATSAPP_APP_SECRET"]
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
when "retail_store"
|
|
56
|
-
# Load from database
|
|
57
|
-
tenant_config = WhatsappConfiguration.find_by(tenant: tenant)
|
|
58
|
-
FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
59
|
-
config.access_token = tenant_config.access_token
|
|
60
|
-
config.phone_number_id = tenant_config.phone_number_id
|
|
61
|
-
config.verify_token = tenant_config.verify_token
|
|
62
|
-
config.app_secret = tenant_config.app_secret
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
else
|
|
66
|
-
FlowChat::Whatsapp::Configuration.from_credentials
|
|
49
|
+
when "acme_corp" then whatsapp_config_from_env("ACME")
|
|
50
|
+
when "tech_startup" then whatsapp_config_from_env("TECHSTARTUP")
|
|
51
|
+
when "retail_store" then config_from_database(tenant)
|
|
52
|
+
else FlowChat::Whatsapp::Configuration.from_credentials
|
|
67
53
|
end
|
|
68
54
|
end
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
else
|
|
79
|
-
WelcomeFlow
|
|
56
|
+
# Load a tenant's credentials from your own model.
|
|
57
|
+
def config_from_database(tenant)
|
|
58
|
+
record = WhatsappConfiguration.find_by!(tenant: tenant)
|
|
59
|
+
FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
|
|
60
|
+
config.access_token = record.access_token
|
|
61
|
+
config.phone_number_id = record.phone_number_id
|
|
62
|
+
config.verify_token = record.verify_token
|
|
63
|
+
config.app_secret = record.app_secret
|
|
80
64
|
end
|
|
81
65
|
end
|
|
66
|
+
|
|
67
|
+
def flow_for_tenant(tenant)
|
|
68
|
+
{
|
|
69
|
+
"acme_corp" => AcmeCorpFlow,
|
|
70
|
+
"tech_startup" => TechStartupFlow,
|
|
71
|
+
"retail_store" => RetailStoreFlow
|
|
72
|
+
}.fetch(tenant, WhatsappWelcomeFlow)
|
|
73
|
+
end
|
|
82
74
|
end
|
|
83
75
|
|
|
84
|
-
#
|
|
76
|
+
# Resolve the account from the webhook's own phone_number_id, rather than the URL.
|
|
85
77
|
class DatabaseWhatsappController < ApplicationController
|
|
78
|
+
include WhatsappConfigBuilder
|
|
79
|
+
|
|
86
80
|
skip_forgery_protection
|
|
87
81
|
|
|
88
82
|
def webhook
|
|
89
|
-
|
|
90
|
-
return head :not_found if
|
|
91
|
-
|
|
92
|
-
whatsapp_config = FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
93
|
-
config.access_token =
|
|
94
|
-
config.phone_number_id =
|
|
95
|
-
config.verify_token =
|
|
96
|
-
config.app_secret =
|
|
83
|
+
account = find_business_account(params)
|
|
84
|
+
return head :not_found if account.nil?
|
|
85
|
+
|
|
86
|
+
whatsapp_config = FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
|
|
87
|
+
config.access_token = account.whatsapp_access_token
|
|
88
|
+
config.phone_number_id = account.whatsapp_phone_number_id
|
|
89
|
+
config.verify_token = account.whatsapp_verify_token
|
|
90
|
+
config.app_secret = account.whatsapp_app_secret
|
|
97
91
|
end
|
|
98
92
|
|
|
99
|
-
processor = FlowChat::
|
|
93
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
100
94
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
101
95
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
102
96
|
end
|
|
103
97
|
|
|
104
|
-
processor.run
|
|
98
|
+
processor.run account.flow_class.constantize, :main_page
|
|
105
99
|
end
|
|
106
100
|
|
|
107
101
|
private
|
|
108
102
|
|
|
109
103
|
def find_business_account(params)
|
|
110
|
-
|
|
111
|
-
phone_number_id = extract_phone_number_id_from_webhook(params)
|
|
104
|
+
phone_number_id = params.dig(:entry, 0, :changes, 0, :value, :metadata, :phone_number_id)
|
|
112
105
|
BusinessAccount.find_by(whatsapp_phone_number_id: phone_number_id)
|
|
113
106
|
end
|
|
114
|
-
|
|
115
|
-
def extract_phone_number_id_from_webhook(params)
|
|
116
|
-
# Extract from webhook payload - implement based on your structure
|
|
117
|
-
params.dig(:entry, 0, :changes, 0, :value, :metadata, :phone_number_id)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Example: Environment-based Configuration
|
|
122
|
-
class EnvironmentWhatsappController < ApplicationController
|
|
123
|
-
skip_forgery_protection
|
|
124
|
-
|
|
125
|
-
def webhook
|
|
126
|
-
# Different configurations for different environments
|
|
127
|
-
whatsapp_config = case Rails.env
|
|
128
|
-
when "production"
|
|
129
|
-
production_whatsapp_config
|
|
130
|
-
when "staging"
|
|
131
|
-
staging_whatsapp_config
|
|
132
|
-
when "development"
|
|
133
|
-
development_whatsapp_config
|
|
134
|
-
else
|
|
135
|
-
FlowChat::Whatsapp::Configuration.from_credentials
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
|
139
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
140
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
processor.run WelcomeFlow, :main_page
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
private
|
|
147
|
-
|
|
148
|
-
def production_whatsapp_config
|
|
149
|
-
FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
150
|
-
config.access_token = ENV["PROD_WHATSAPP_ACCESS_TOKEN"]
|
|
151
|
-
config.phone_number_id = ENV["PROD_WHATSAPP_PHONE_NUMBER_ID"]
|
|
152
|
-
config.verify_token = ENV["PROD_WHATSAPP_VERIFY_TOKEN"]
|
|
153
|
-
config.app_id = ENV["PROD_WHATSAPP_APP_ID"]
|
|
154
|
-
config.app_secret = ENV["PROD_WHATSAPP_APP_SECRET"]
|
|
155
|
-
config.business_account_id = ENV["PROD_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def staging_whatsapp_config
|
|
160
|
-
FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
161
|
-
config.access_token = ENV["STAGING_WHATSAPP_ACCESS_TOKEN"]
|
|
162
|
-
config.phone_number_id = ENV["STAGING_WHATSAPP_PHONE_NUMBER_ID"]
|
|
163
|
-
config.verify_token = ENV["STAGING_WHATSAPP_VERIFY_TOKEN"]
|
|
164
|
-
config.app_id = ENV["STAGING_WHATSAPP_APP_ID"]
|
|
165
|
-
config.app_secret = ENV["STAGING_WHATSAPP_APP_SECRET"]
|
|
166
|
-
config.business_account_id = ENV["STAGING_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def development_whatsapp_config
|
|
171
|
-
FlowChat::Whatsapp::Configuration.new.tap do |config|
|
|
172
|
-
config.access_token = ENV["DEV_WHATSAPP_ACCESS_TOKEN"]
|
|
173
|
-
config.phone_number_id = ENV["DEV_WHATSAPP_PHONE_NUMBER_ID"]
|
|
174
|
-
config.verify_token = ENV["DEV_WHATSAPP_VERIFY_TOKEN"]
|
|
175
|
-
config.app_id = ENV["DEV_WHATSAPP_APP_ID"]
|
|
176
|
-
config.app_secret = ENV["DEV_WHATSAPP_APP_SECRET"]
|
|
177
|
-
config.business_account_id = ENV["DEV_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Example: Simple Custom Configuration
|
|
183
|
-
class CustomWhatsappController < ApplicationController
|
|
184
|
-
skip_forgery_protection
|
|
185
|
-
|
|
186
|
-
def webhook
|
|
187
|
-
# Create custom configuration for this specific endpoint
|
|
188
|
-
my_config = FlowChat::Whatsapp::Configuration.new
|
|
189
|
-
my_config.access_token = "EAABs..." # Your specific access token
|
|
190
|
-
my_config.phone_number_id = "123456789"
|
|
191
|
-
my_config.verify_token = "my_verify_token"
|
|
192
|
-
my_config.app_id = "your_app_id"
|
|
193
|
-
my_config.app_secret = "your_app_secret"
|
|
194
|
-
my_config.business_account_id = "your_business_account_id"
|
|
195
|
-
|
|
196
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
|
197
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, my_config
|
|
198
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
processor.run CustomFlow, :main_page
|
|
202
|
-
end
|
|
203
107
|
end
|
|
204
108
|
|
|
205
|
-
# Add routes for
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
109
|
+
# Add routes for the tenant endpoints, for example:
|
|
110
|
+
# Rails.application.routes.draw do
|
|
111
|
+
# constraints subdomain: /\w+/ do
|
|
112
|
+
# post "/whatsapp/webhook", to: "multi_tenant_whatsapp#webhook"
|
|
113
|
+
# end
|
|
114
|
+
# post "/whatsapp/:tenant/webhook", to: "multi_tenant_whatsapp#webhook"
|
|
210
115
|
# end
|
|
211
|
-
#
|
|
212
|
-
# # Path-based routing
|
|
213
|
-
# post '/whatsapp/:tenant/webhook', to: 'multi_tenant_whatsapp#webhook'
|
|
214
|
-
#
|
|
215
|
-
# # Environment-specific
|
|
216
|
-
# post '/whatsapp/env/webhook', to: 'environment_whatsapp#webhook'
|
|
217
|
-
#
|
|
218
|
-
# # Custom endpoint
|
|
219
|
-
# post '/whatsapp/custom/webhook', to: 'custom_whatsapp#webhook'
|
|
220
|
-
# end
|
|
@@ -102,5 +102,4 @@ end
|
|
|
102
102
|
# - Different API versions (v1, v2, etc.)
|
|
103
103
|
# - Multi-tenant endpoints with different configurations
|
|
104
104
|
# - Legacy endpoints alongside new ones
|
|
105
|
-
# - External HTTP servers (run examples/http_simulator_test.rb)
|
|
106
105
|
# - Different flow implementations for different endpoints
|
data/examples/ussd_controller.rb
CHANGED
|
@@ -5,49 +5,83 @@ class UssdController < ApplicationController
|
|
|
5
5
|
skip_forgery_protection
|
|
6
6
|
|
|
7
7
|
def process_request
|
|
8
|
-
processor = FlowChat::
|
|
8
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
9
9
|
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
11
|
+
|
|
12
|
+
# Durable sessions key on the phone number, so a conversation survives the
|
|
13
|
+
# telco rotating its session id on timeout. Optional.
|
|
14
|
+
config.use_durable_sessions
|
|
12
15
|
|
|
13
|
-
#
|
|
14
|
-
|
|
16
|
+
# Other options you can set here:
|
|
17
|
+
#
|
|
18
|
+
# config.use_middleware LoggingMiddleware # custom middleware (see below)
|
|
19
|
+
# config.use_session_config( # explicit session boundaries
|
|
20
|
+
# boundaries: [:flow, :platform],
|
|
21
|
+
# hash_identifiers: true # hash phone numbers for privacy
|
|
22
|
+
# )
|
|
15
23
|
end
|
|
16
24
|
|
|
17
|
-
processor.run
|
|
25
|
+
processor.run UssdWelcomeFlow, :main_page
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add this route to your config/routes.rb:
|
|
30
|
+
# post "/ussd", to: "ussd#process_request"
|
|
31
|
+
|
|
32
|
+
# Pagination is configured globally, not per processor. USSD messages are
|
|
33
|
+
# length-limited, so long output is split into pages with navigation options:
|
|
34
|
+
#
|
|
35
|
+
# FlowChat::Config.ussd.pagination_page_size = 120
|
|
36
|
+
# FlowChat::Config.ussd.pagination_next_option = "#"
|
|
37
|
+
# FlowChat::Config.ussd.pagination_back_option = "0"
|
|
38
|
+
|
|
39
|
+
# A custom middleware, added above with config.use_middleware. It sees the
|
|
40
|
+
# normalized context on the way in and the [type, prompt, choices, media] result
|
|
41
|
+
# on the way out.
|
|
42
|
+
class LoggingMiddleware
|
|
43
|
+
def initialize(app)
|
|
44
|
+
@app = app
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call(context)
|
|
48
|
+
Rails.logger.info "USSD request from #{context["request.msisdn"]}: #{context.input}"
|
|
49
|
+
start_time = Time.current
|
|
50
|
+
|
|
51
|
+
result = @app.call(context)
|
|
52
|
+
|
|
53
|
+
duration = Time.current - start_time
|
|
54
|
+
Rails.logger.info "USSD response (#{duration.round(3)}s): #{result[1]}"
|
|
55
|
+
|
|
56
|
+
result
|
|
18
57
|
end
|
|
19
58
|
end
|
|
20
59
|
|
|
21
|
-
# Example
|
|
22
|
-
# Add this to your Rails application as app/flow_chat/
|
|
60
|
+
# Example flow for USSD
|
|
61
|
+
# Add this to your Rails application as app/flow_chat/ussd_welcome_flow.rb
|
|
23
62
|
|
|
24
|
-
class
|
|
63
|
+
class UssdWelcomeFlow < FlowChat::Flow
|
|
25
64
|
def main_page
|
|
26
|
-
# Welcome the user
|
|
27
65
|
name = app.screen(:name) do |prompt|
|
|
28
66
|
prompt.ask "Welcome! What's your name?",
|
|
29
67
|
transform: ->(input) { input.strip.titleize }
|
|
30
68
|
end
|
|
31
69
|
|
|
32
|
-
#
|
|
70
|
+
# Choices render as a numbered list; the flow works in the choice keys.
|
|
33
71
|
choice = app.screen(:main_menu) do |prompt|
|
|
34
72
|
prompt.select "Hi #{name}! Choose:", {
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
73
|
+
"info" => "Account Info",
|
|
74
|
+
"payment" => "Make Payment",
|
|
75
|
+
"balance" => "Get Balance",
|
|
76
|
+
"support" => "Support"
|
|
39
77
|
}
|
|
40
78
|
end
|
|
41
79
|
|
|
42
80
|
case choice
|
|
43
|
-
when "
|
|
44
|
-
|
|
45
|
-
when "
|
|
46
|
-
|
|
47
|
-
when "3"
|
|
48
|
-
get_balance
|
|
49
|
-
when "4"
|
|
50
|
-
customer_support
|
|
81
|
+
when "info" then show_account_info
|
|
82
|
+
when "payment" then make_payment
|
|
83
|
+
when "balance" then get_balance
|
|
84
|
+
when "support" then customer_support
|
|
51
85
|
end
|
|
52
86
|
end
|
|
53
87
|
|
|
@@ -56,21 +90,21 @@ class WelcomeFlow < FlowChat::Flow
|
|
|
56
90
|
def show_account_info
|
|
57
91
|
info_choice = app.screen(:account_info) do |prompt|
|
|
58
92
|
prompt.select "Account Info:", {
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
93
|
+
"details" => "Personal Details",
|
|
94
|
+
"balance" => "Balance",
|
|
95
|
+
"history" => "Transaction History",
|
|
96
|
+
"back" => "Main Menu"
|
|
63
97
|
}
|
|
64
98
|
end
|
|
65
99
|
|
|
66
100
|
case info_choice
|
|
67
|
-
when "
|
|
68
|
-
app.say "Name: John Doe\nPhone: #{app.
|
|
69
|
-
when "
|
|
101
|
+
when "details"
|
|
102
|
+
app.say "Name: John Doe\nPhone: #{app.msisdn}\nStatus: Active"
|
|
103
|
+
when "balance"
|
|
70
104
|
app.say "Balance: $150.75\nCredit: $1,000.00"
|
|
71
|
-
when "
|
|
105
|
+
when "history"
|
|
72
106
|
app.say "Recent:\n+$50.00 Deposit\n-$25.50 Purchase\n-$15.00 Transfer"
|
|
73
|
-
when "
|
|
107
|
+
when "back"
|
|
74
108
|
main_page
|
|
75
109
|
end
|
|
76
110
|
end
|
|
@@ -79,9 +113,9 @@ class WelcomeFlow < FlowChat::Flow
|
|
|
79
113
|
amount = app.screen(:payment_amount) do |prompt|
|
|
80
114
|
prompt.ask "Enter amount:",
|
|
81
115
|
validate: ->(input) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
amount = input.to_f
|
|
117
|
+
next "Invalid amount" unless amount > 0
|
|
118
|
+
next "Max $500" unless amount <= 500
|
|
85
119
|
nil
|
|
86
120
|
},
|
|
87
121
|
transform: ->(input) { input.to_f }
|
|
@@ -89,19 +123,14 @@ class WelcomeFlow < FlowChat::Flow
|
|
|
89
123
|
|
|
90
124
|
recipient = app.screen(:payment_recipient) do |prompt|
|
|
91
125
|
prompt.ask "Recipient phone:",
|
|
92
|
-
validate: ->(input) {
|
|
93
|
-
return "10 digits required" unless input.match?(/\A\d{10}\z/)
|
|
94
|
-
nil
|
|
95
|
-
}
|
|
126
|
+
validate: ->(input) { "10 digits required" unless input.match?(/\A\d{10}\z/) }
|
|
96
127
|
end
|
|
97
128
|
|
|
98
|
-
# Confirmation screen
|
|
99
129
|
confirmed = app.screen(:payment_confirmation) do |prompt|
|
|
100
130
|
prompt.yes? "Pay $#{amount} to #{recipient}?"
|
|
101
131
|
end
|
|
102
132
|
|
|
103
133
|
if confirmed
|
|
104
|
-
# Process payment (your business logic here)
|
|
105
134
|
transaction_id = process_payment(amount, recipient)
|
|
106
135
|
app.say "Payment successful!\nID: #{transaction_id}\nAmount: $#{amount}"
|
|
107
136
|
else
|
|
@@ -110,26 +139,25 @@ class WelcomeFlow < FlowChat::Flow
|
|
|
110
139
|
end
|
|
111
140
|
|
|
112
141
|
def get_balance
|
|
113
|
-
|
|
114
|
-
balance = check_account_balance(app.phone_number)
|
|
142
|
+
balance = check_account_balance(app.msisdn)
|
|
115
143
|
app.say "Balance\n\nAvailable: $#{balance[:available]}\nPending: $#{balance[:pending]}"
|
|
116
144
|
end
|
|
117
145
|
|
|
118
146
|
def customer_support
|
|
119
147
|
support_choice = app.screen(:support_menu) do |prompt|
|
|
120
148
|
prompt.select "Support:", {
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
149
|
+
"report" => "Report Issue",
|
|
150
|
+
"contact" => "Contact Info",
|
|
151
|
+
"back" => "Main Menu"
|
|
124
152
|
}
|
|
125
153
|
end
|
|
126
154
|
|
|
127
155
|
case support_choice
|
|
128
|
-
when "
|
|
156
|
+
when "report"
|
|
129
157
|
report_issue
|
|
130
|
-
when "
|
|
158
|
+
when "contact"
|
|
131
159
|
app.say "Support:\nCall: 123-456-7890\nEmail: support@company.com\nHours: 9AM-5PM"
|
|
132
|
-
when "
|
|
160
|
+
when "back"
|
|
133
161
|
main_page
|
|
134
162
|
end
|
|
135
163
|
end
|
|
@@ -137,134 +165,34 @@ class WelcomeFlow < FlowChat::Flow
|
|
|
137
165
|
def report_issue
|
|
138
166
|
issue_type = app.screen(:issue_type) do |prompt|
|
|
139
167
|
prompt.select "Issue type:", {
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
168
|
+
"payment" => "Payment Problem",
|
|
169
|
+
"access" => "Account Access",
|
|
170
|
+
"error" => "Service Error",
|
|
171
|
+
"other" => "Other"
|
|
144
172
|
}
|
|
145
173
|
end
|
|
146
174
|
|
|
147
175
|
description = app.screen(:issue_description) do |prompt|
|
|
148
176
|
prompt.ask "Describe issue:",
|
|
149
|
-
validate: ->(input) {
|
|
150
|
-
return "Min 10 characters" if input.length < 10
|
|
151
|
-
nil
|
|
152
|
-
}
|
|
177
|
+
validate: ->(input) { "Min 10 characters" if input.length < 10 }
|
|
153
178
|
end
|
|
154
179
|
|
|
155
|
-
|
|
156
|
-
ticket_id = create_support_ticket(issue_type, description, app.phone_number)
|
|
157
|
-
|
|
180
|
+
ticket_id = create_support_ticket(issue_type, description, app.msisdn)
|
|
158
181
|
app.say "Issue reported!\n\nTicket: #{ticket_id}\nWe'll contact you within 24hrs"
|
|
159
182
|
end
|
|
160
183
|
|
|
161
|
-
#
|
|
184
|
+
# Replace these with your own business logic.
|
|
162
185
|
|
|
163
186
|
def process_payment(amount, recipient)
|
|
164
|
-
# Your payment processing logic here
|
|
165
|
-
# Return transaction ID
|
|
166
187
|
"TXN#{rand(100000..999999)}"
|
|
167
188
|
end
|
|
168
189
|
|
|
169
|
-
def check_account_balance(
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
available: "150.75",
|
|
173
|
-
pending: "25.00"
|
|
174
|
-
}
|
|
190
|
+
def check_account_balance(msisdn)
|
|
191
|
+
{available: "150.75", pending: "25.00"}
|
|
175
192
|
end
|
|
176
193
|
|
|
177
|
-
def create_support_ticket(issue_type, description,
|
|
178
|
-
#
|
|
179
|
-
Rails.logger.info "Ticket: #{issue_type} - #{description} from #{phone_number}"
|
|
194
|
+
def create_support_ticket(issue_type, description, msisdn)
|
|
195
|
+
Rails.logger.info "Ticket: #{issue_type} - #{description} from #{msisdn}"
|
|
180
196
|
"TICKET#{rand(10000..99999)}"
|
|
181
197
|
end
|
|
182
198
|
end
|
|
183
|
-
|
|
184
|
-
# Configuration Examples:
|
|
185
|
-
|
|
186
|
-
# 1. Basic configuration with custom pagination
|
|
187
|
-
# rubocop:disable Lint/DuplicateMethods
|
|
188
|
-
class UssdController < ApplicationController
|
|
189
|
-
skip_forgery_protection
|
|
190
|
-
|
|
191
|
-
def process_request
|
|
192
|
-
# Configure pagination for shorter messages
|
|
193
|
-
FlowChat::Config.ussd.pagination_page_size = 120
|
|
194
|
-
FlowChat::Config.ussd.pagination_next_option = "#"
|
|
195
|
-
FlowChat::Config.ussd.pagination_back_option = "*"
|
|
196
|
-
|
|
197
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
198
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
199
|
-
config.use_session_store FlowChat::Session::RailsSessionStore
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
processor.run WelcomeFlow, :main_page
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
# rubocop:enable Lint/DuplicateMethods
|
|
206
|
-
|
|
207
|
-
# 2. Configuration with custom middleware
|
|
208
|
-
class LoggingMiddleware
|
|
209
|
-
def initialize(app)
|
|
210
|
-
@app = app
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def call(context)
|
|
214
|
-
Rails.logger.info "USSD Request from #{context["request.msisdn"]}: #{context.input}"
|
|
215
|
-
start_time = Time.current
|
|
216
|
-
|
|
217
|
-
result = @app.call(context)
|
|
218
|
-
|
|
219
|
-
duration = Time.current - start_time
|
|
220
|
-
Rails.logger.info "USSD Response (#{duration.round(3)}s): #{result[1]}"
|
|
221
|
-
|
|
222
|
-
result
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# rubocop:disable Lint/DuplicateMethods
|
|
227
|
-
class UssdController < ApplicationController
|
|
228
|
-
skip_forgery_protection
|
|
229
|
-
|
|
230
|
-
def process_request
|
|
231
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
232
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
233
|
-
config.use_session_store FlowChat::Session::RailsSessionStore
|
|
234
|
-
config.use_middleware LoggingMiddleware # Add custom logging
|
|
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_identifiers: true # hash phone numbers for privacy
|
|
241
|
-
# )
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
processor.run WelcomeFlow, :main_page
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
# rubocop:enable Lint/DuplicateMethods
|
|
248
|
-
|
|
249
|
-
# 3. Configuration with cache-based sessions for longer persistence
|
|
250
|
-
# rubocop:disable Lint/DuplicateMethods
|
|
251
|
-
class UssdController < ApplicationController
|
|
252
|
-
skip_forgery_protection
|
|
253
|
-
|
|
254
|
-
def process_request
|
|
255
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
256
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
257
|
-
# Use cache store for longer session persistence
|
|
258
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
processor.run WelcomeFlow, :main_page
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
# rubocop:enable Lint/DuplicateMethods
|
|
265
|
-
|
|
266
|
-
# Add this route to your config/routes.rb:
|
|
267
|
-
# post '/ussd', to: 'ussd#process_request'
|
|
268
|
-
|
|
269
|
-
# For Nsano gateway, use:
|
|
270
|
-
# config.use_gateway FlowChat::Ussd::Gateway::Nsano
|