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
data/docs/configuration.md
CHANGED
|
@@ -1,360 +1,183 @@
|
|
|
1
|
-
# Configuration
|
|
1
|
+
# Configuration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
FlowChat has two layers of configuration: global settings on `FlowChat::Config`, set once during boot, and per-processor settings passed to the `FlowChat::Processor.new` block for each webhook.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Global configuration
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# config/initializers/flowchat.rb
|
|
9
|
-
|
|
10
|
-
# Core configuration
|
|
11
|
-
FlowChat::Config.logger = Rails.logger
|
|
12
|
-
FlowChat::Config.cache = Rails.cache
|
|
13
|
-
FlowChat::Config.simulator_secret = "your_secure_secret_here"
|
|
14
|
-
|
|
15
|
-
# Validation error display behavior
|
|
16
|
-
FlowChat::Config.combine_validation_error_with_message = true # default
|
|
7
|
+
Set these once, for example in `config/initializers/flow_chat.rb`.
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
| Option | Default | What it controls |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `FlowChat::Config.logger` | `Logger.new($stdout)` | The logger FlowChat writes to. Set it to `Rails.logger` to fold FlowChat logs into your app's. |
|
|
12
|
+
| `FlowChat::Config.cache` | `nil` | The cache backend the session store reads and writes. Required: `CacheSessionStore` raises without it. Set it to `Rails.cache` or any store with the same interface. |
|
|
13
|
+
| `FlowChat::Config.simulator_secret` | `nil` | Secret that enables the local web simulator. The simulator stays off until this is set. See [testing.md](testing.md). |
|
|
14
|
+
| `FlowChat::Config.combine_validation_error_with_message` | `true` | When `true`, a rejected input re-prompts with the validation error followed by the original prompt. When `false`, only the error is shown. |
|
|
15
|
+
| `FlowChat::Config.inject_middleware_logger` | `true` in Rails development, else `false` | Whether a logging middleware is inserted into the stack automatically. |
|
|
23
16
|
|
|
24
17
|
```ruby
|
|
25
|
-
#
|
|
26
|
-
FlowChat::Config.
|
|
27
|
-
FlowChat::Config.
|
|
28
|
-
FlowChat::Config.
|
|
29
|
-
|
|
30
|
-
# Available boundary options:
|
|
31
|
-
# :flow - separate sessions per flow class
|
|
32
|
-
# :platform - separate sessions per platform (ussd, whatsapp)
|
|
33
|
-
# :gateway - separate sessions per gateway
|
|
34
|
-
# [] - global sessions (no boundaries)
|
|
35
|
-
|
|
36
|
-
# Available identifier options:
|
|
37
|
-
# nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
|
|
38
|
-
# :msisdn - use phone number (durable sessions)
|
|
39
|
-
# :request_id - use request ID (ephemeral sessions)
|
|
18
|
+
# config/initializers/flow_chat.rb
|
|
19
|
+
FlowChat::Config.cache = Rails.cache
|
|
20
|
+
FlowChat::Config.logger = Rails.logger
|
|
21
|
+
FlowChat::Config.simulator_secret = Rails.application.credentials.flow_chat_simulator_secret
|
|
40
22
|
```
|
|
41
23
|
|
|
42
|
-
## USSD
|
|
24
|
+
## USSD configuration
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
# USSD pagination settings
|
|
46
|
-
FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
|
|
47
|
-
FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next page
|
|
48
|
-
FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
|
|
49
|
-
FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
|
|
50
|
-
FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
|
|
51
|
-
```
|
|
26
|
+
`FlowChat::Config.ussd` controls how USSD responses are paginated. USSD messages are length-limited, so FlowChat splits long output into pages and adds navigation options.
|
|
52
27
|
|
|
53
|
-
|
|
28
|
+
| Option | Default | What it controls |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `pagination_page_size` | `140` | Maximum characters per USSD page before FlowChat splits the response. |
|
|
31
|
+
| `pagination_next_option` | `"#"` | The input a user sends to see the next page. |
|
|
32
|
+
| `pagination_next_text` | `"More"` | The label shown next to the next-page option. |
|
|
33
|
+
| `pagination_back_option` | `"0"` | The input a user sends to go to the previous page. |
|
|
34
|
+
| `pagination_back_text` | `"Back"` | The label shown next to the previous-page option. |
|
|
54
35
|
|
|
55
36
|
```ruby
|
|
56
|
-
|
|
57
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
|
|
58
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
|
37
|
+
FlowChat::Config.ussd.pagination_page_size = 160
|
|
59
38
|
```
|
|
60
39
|
|
|
61
|
-
|
|
40
|
+
## WhatsApp configuration
|
|
62
41
|
|
|
63
|
-
|
|
42
|
+
`FlowChat::Config.whatsapp` holds the WhatsApp Cloud API base URL. Per-tenant credentials (access token, phone number id, app secret, verify token) are passed to the gateway, not set here. See [platforms/whatsapp.md](platforms/whatsapp.md).
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
44
|
+
| Option | Default | What it controls |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `api_base_url` | `"https://graph.facebook.com/v23.0"` | The Cloud API version and host the WhatsApp client calls. |
|
|
68
47
|
|
|
69
|
-
|
|
70
|
-
whatsapp:
|
|
71
|
-
access_token: "your_access_token"
|
|
72
|
-
phone_number_id: "your_phone_number_id"
|
|
73
|
-
verify_token: "your_verify_token"
|
|
74
|
-
app_id: "your_app_id"
|
|
75
|
-
app_secret: "your_app_secret"
|
|
76
|
-
business_account_id: "your_business_account_id"
|
|
77
|
-
skip_signature_validation: false
|
|
78
|
-
```
|
|
48
|
+
## HTTP configuration
|
|
79
49
|
|
|
80
|
-
|
|
50
|
+
`FlowChat::Config.http` holds defaults for the HTTP gateway used in testing and custom integrations. See the platform guides for the full request and response shapes.
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
export WHATSAPP_APP_SECRET="your_app_secret"
|
|
88
|
-
export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
|
|
89
|
-
export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
|
|
90
|
-
```
|
|
52
|
+
| Option | Default | What it controls |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `default_gateway` | `:simple` | The HTTP gateway used when none is named. |
|
|
55
|
+
| `request_timeout` | `30` | Request timeout in seconds. |
|
|
56
|
+
| `response_format` | `:json` | The response serialization format. |
|
|
91
57
|
|
|
92
|
-
|
|
58
|
+
## Sessions
|
|
93
59
|
|
|
94
|
-
|
|
95
|
-
config = FlowChat::Whatsapp::Configuration.new(:my_config) # Named configuration
|
|
96
|
-
config.access_token = "your_access_token"
|
|
97
|
-
config.phone_number_id = "your_phone_number_id"
|
|
98
|
-
config.verify_token = "your_verify_token"
|
|
99
|
-
config.app_id = "your_app_id"
|
|
100
|
-
config.app_secret = "your_app_secret"
|
|
101
|
-
config.business_account_id = "your_business_account_id"
|
|
102
|
-
config.skip_signature_validation = false
|
|
103
|
-
# Configuration is automatically registered as :my_config
|
|
104
|
-
```
|
|
60
|
+
A session holds the answers a flow has collected so far. FlowChat looks the session up by an id it builds on every request from two things: a set of boundaries and an identifier.
|
|
105
61
|
|
|
106
|
-
|
|
62
|
+
`FlowChat::Config.session` sets the defaults.
|
|
107
63
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
staging_config = FlowChat::Whatsapp::Configuration.new(:staging)
|
|
116
|
-
staging_config.access_token = ENV['STAGING_WHATSAPP_TOKEN']
|
|
117
|
-
# ... other settings
|
|
118
|
-
```
|
|
64
|
+
| Option | Default | What it controls |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `boundaries` | `[:flow, :gateway, :platform]` | Which dimensions separate one session from another (see below). |
|
|
67
|
+
| `hash_identifiers` | `true` | Whether the identifier (often a phone number) is hashed into the session id rather than stored in the clear. |
|
|
68
|
+
| `identifier` | `nil` | Which request value identifies the user. `nil` lets the platform choose: WhatsApp uses `:msisdn`, the others use `:request_id`. |
|
|
69
|
+
| `session_id_proc` | `nil` | A callable that builds the session id from the context directly, bypassing boundaries and identifier. |
|
|
119
70
|
|
|
120
|
-
|
|
71
|
+
### How the session id is built
|
|
121
72
|
|
|
122
|
-
|
|
123
|
-
# Use registered configuration
|
|
124
|
-
config = FlowChat::Whatsapp::Configuration.get(:production)
|
|
125
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
|
126
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
|
|
127
|
-
end
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## Security Configuration
|
|
73
|
+
FlowChat sets the session id one of three ways, in order of precedence:
|
|
131
74
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# Production security (recommended)
|
|
136
|
-
config.app_secret = "your_whatsapp_app_secret"
|
|
137
|
-
config.skip_signature_validation = false # default
|
|
75
|
+
1. If `context["session.id"]` is already set, that value is used verbatim (a manual override).
|
|
76
|
+
2. If you passed a block to `use_session_config`, its return value is used verbatim. Boundaries and identifier are skipped entirely.
|
|
77
|
+
3. Otherwise the id is built from the boundaries and the identifier, described below.
|
|
138
78
|
|
|
139
|
-
|
|
140
|
-
config.app_secret = nil
|
|
141
|
-
config.skip_signature_validation = true
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Simulator Security
|
|
145
|
-
|
|
146
|
-
```ruby
|
|
147
|
-
# Use Rails secret for uniqueness
|
|
148
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
|
79
|
+
Think of the built id as `boundaries` followed by `identifier`: the boundaries describe the context a session lives in (the walls that separate it), and the identifier is who the session belongs to. The parts are joined with `:` in this fixed order, and a part is included only when its boundary is enabled and its value is present:
|
|
149
80
|
|
|
150
|
-
# Or use dedicated secret
|
|
151
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
|
152
81
|
```
|
|
153
|
-
|
|
154
|
-
## Environment-Specific Configuration
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
# config/initializers/flowchat.rb
|
|
158
|
-
case Rails.env
|
|
159
|
-
when 'development'
|
|
160
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
|
161
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
|
162
|
-
|
|
163
|
-
when 'test'
|
|
164
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
|
165
|
-
FlowChat::Config.simulator_secret = "test_secret"
|
|
166
|
-
|
|
167
|
-
when 'staging'
|
|
168
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
|
169
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
|
170
|
-
|
|
171
|
-
when 'production'
|
|
172
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
|
173
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
|
174
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
|
175
|
-
end
|
|
82
|
+
flow_name : platform : gateway : url : identifier
|
|
176
83
|
```
|
|
177
84
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
### USSD Processor
|
|
85
|
+
The order of the parts is fixed regardless of the order you list boundaries in. With the default boundaries `[:flow, :gateway, :platform]`, a WhatsApp user in `RegistrationFlow` gets:
|
|
181
86
|
|
|
182
|
-
```ruby
|
|
183
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
184
|
-
# Gateway (required)
|
|
185
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
186
|
-
|
|
187
|
-
# Session storage (required)
|
|
188
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
189
|
-
|
|
190
|
-
# Optional middleware
|
|
191
|
-
config.use_middleware MyCustomMiddleware
|
|
192
|
-
|
|
193
|
-
# Configure session boundaries
|
|
194
|
-
config.use_session_config(
|
|
195
|
-
boundaries: [:flow, :platform], # which boundaries to enforce
|
|
196
|
-
hash_identifiers: true, # hash identifiers for privacy
|
|
197
|
-
identifier: :msisdn # use MSISDN for durable sessions (optional)
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# Shorthand for durable sessions (identifier: :msisdn)
|
|
201
|
-
config.use_durable_sessions
|
|
202
|
-
end
|
|
203
87
|
```
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
|
|
209
|
-
# Gateway (required)
|
|
210
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
211
|
-
|
|
212
|
-
# Session storage (required)
|
|
213
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
214
|
-
|
|
215
|
-
# Optional custom configuration
|
|
216
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_whatsapp_config
|
|
217
|
-
end
|
|
88
|
+
registration_flow:whatsapp:whatsapp_cloud_api:9f86d0818...
|
|
89
|
+
(flow) (platform) (gateway) (hashed msisdn)
|
|
218
90
|
```
|
|
219
91
|
|
|
220
|
-
|
|
92
|
+
The hashed identifier is shown truncated here for readability; the real key carries the 32-character (128-bit) SHA256 prefix.
|
|
221
93
|
|
|
222
|
-
###
|
|
94
|
+
### What each boundary isolates
|
|
223
95
|
|
|
224
|
-
|
|
225
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
226
|
-
```
|
|
96
|
+
A boundary is a wall. Include it and two requests that differ on that dimension get separate sessions; drop it and they share one session, provided the identifier matches.
|
|
227
97
|
|
|
228
|
-
|
|
98
|
+
| Boundary | Segment | Adding it separates sessions by | Drop it when |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `:flow` | `context["flow.name"]` | flow class, so `RegistrationFlow` and `SurveyFlow` never share state | you want one session shared across all flows (rare) |
|
|
101
|
+
| `:platform` | `:ussd`, `:whatsapp`, and so on | platform, so the same person on USSD and on WhatsApp gets two sessions | you want one conversation to span platforms |
|
|
102
|
+
| `:gateway` | `:nalo`, `:whatsapp_cloud_api`, and so on | gateway, so two gateways on the same platform (for example two USSD aggregators) do not collide | you are consolidating gateways and want them to share |
|
|
103
|
+
| `:url` | normalized `host + path` | host and path, for per-tenant or per-endpoint isolation (`tenant1.app.com` vs `tenant2.app.com`) | single tenant, single endpoint |
|
|
229
104
|
|
|
230
|
-
|
|
105
|
+
The `:url` segment is `host + path` with the leading slash removed and any character outside `[a-zA-Z0-9._-]` replaced by `_`. If that exceeds 50 characters it becomes the first 41 characters plus an 8-character SHA256 suffix, so it stays bounded but still recognizable.
|
|
231
106
|
|
|
232
|
-
###
|
|
107
|
+
### The identifier
|
|
233
108
|
|
|
234
|
-
|
|
235
|
-
# Pagination (USSD only, automatic)
|
|
236
|
-
FlowChat::Ussd::Middleware::Pagination
|
|
109
|
+
The identifier is chosen independently of the boundaries and always comes last. Its type is the `identifier` option, or a platform default when unset: WhatsApp uses `:msisdn`, every other platform uses `:request_id`.
|
|
237
110
|
|
|
238
|
-
|
|
239
|
-
|
|
111
|
+
| Type | Value | Hashed |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `:request_id` | `context["request.id"]` | Never. It is already opaque. On USSD it is the telco session id, which rotates on timeout, so the session is ephemeral. |
|
|
114
|
+
| `:msisdn` | `context["request.msisdn"]` | Yes, when `hash_identifiers` is true (the default): the first 32 hex characters of SHA256 (128 bits). |
|
|
115
|
+
| `:user_id` | `context["request.user_id"]` | Yes, when `hash_identifiers` is true. |
|
|
240
116
|
|
|
241
|
-
|
|
242
|
-
FlowChat::Ussd::Gateway::Nalo
|
|
243
|
-
FlowChat::Whatsapp::Gateway::CloudApi
|
|
244
|
-
```
|
|
117
|
+
So with the default `hash_identifiers: true`, a phone number never appears in the session key in the clear, while a `request_id` passes through untouched.
|
|
245
118
|
|
|
246
|
-
###
|
|
119
|
+
### Worked examples
|
|
247
120
|
|
|
248
|
-
|
|
249
|
-
class LoggingMiddleware
|
|
250
|
-
def initialize(app)
|
|
251
|
-
@app = app
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def call(context)
|
|
255
|
-
Rails.logger.info "Processing request: #{context.input}"
|
|
256
|
-
result = @app.call(context)
|
|
257
|
-
Rails.logger.info "Response: #{result[1]}"
|
|
258
|
-
result
|
|
259
|
-
end
|
|
260
|
-
end
|
|
121
|
+
Default config, USSD, `RegistrationFlow`, no durable sessions. The identifier is `:request_id`, the telco session id, so the session lasts only as long as that telco session:
|
|
261
122
|
|
|
262
|
-
|
|
263
|
-
|
|
123
|
+
```
|
|
124
|
+
registration_flow:ussd:nalo:1699_telco_session_42
|
|
264
125
|
```
|
|
265
126
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
### Error Display Options
|
|
127
|
+
The same, but with `use_durable_sessions` (identifier `:user_id`, which Nalo sets equal to the msisdn) and hashing on. Now the user can time out and dial back into the same session:
|
|
269
128
|
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
FlowChat::Config.combine_validation_error_with_message = true
|
|
273
|
-
# User sees: "Invalid email format\n\nEnter your email:"
|
|
274
|
-
|
|
275
|
-
# Show only validation error
|
|
276
|
-
FlowChat::Config.combine_validation_error_with_message = false
|
|
277
|
-
# User sees: "Invalid email format"
|
|
129
|
+
```
|
|
130
|
+
registration_flow:ussd:nalo:9f86d0818...
|
|
278
131
|
```
|
|
279
132
|
|
|
280
|
-
|
|
133
|
+
### Configuring sessions per processor
|
|
281
134
|
|
|
282
|
-
|
|
135
|
+
Set boundaries, identifier, and hashing directly:
|
|
283
136
|
|
|
284
137
|
```ruby
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def perform(send_data)
|
|
290
|
-
perform_whatsapp_send(send_data)
|
|
291
|
-
end
|
|
138
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
139
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
140
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
141
|
+
config.use_session_config(boundaries: [:flow, :platform], identifier: :msisdn, hash_identifiers: true)
|
|
292
142
|
end
|
|
293
143
|
```
|
|
294
144
|
|
|
295
|
-
|
|
296
|
-
1. Named configuration from `send_data[:configuration_name]` if present
|
|
297
|
-
2. Default configuration from credentials/environment variables
|
|
298
|
-
|
|
299
|
-
For custom resolution logic, override the configuration resolution:
|
|
145
|
+
Or take full control of the id with a block:
|
|
300
146
|
|
|
301
147
|
```ruby
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def perform(send_data)
|
|
306
|
-
perform_whatsapp_send(send_data)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
private
|
|
310
|
-
|
|
311
|
-
def resolve_whatsapp_configuration(send_data)
|
|
312
|
-
# Custom logic to resolve configuration
|
|
313
|
-
tenant_id = ...
|
|
314
|
-
FlowChat::Whatsapp::Configuration.get("tenant_#{tenant_id}")
|
|
315
|
-
end
|
|
148
|
+
config.use_session_config do |context|
|
|
149
|
+
"tenant:#{context["request.tenant_id"]}:#{context["request.msisdn"]}"
|
|
316
150
|
end
|
|
317
151
|
```
|
|
318
152
|
|
|
319
|
-
|
|
153
|
+
Three convenience methods wrap common changes:
|
|
320
154
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
155
|
+
| Method | Effect |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `use_durable_sessions` | Sets the identifier to `:user_id`, a stable per-user id. It changes only the identifier, not the boundaries. It matters most on USSD, where `:request_id` rotates on timeout but `:user_id` (which Nalo sets equal to the msisdn) is stable, so the conversation survives a rotation. |
|
|
158
|
+
| `use_cross_platform_sessions` | Narrows boundaries to `[:flow]`, dropping platform and gateway, so one user shares a single session across platforms. |
|
|
159
|
+
| `use_url_isolation` | Appends `:url` to the current boundaries for per-tenant or per-host isolation. |
|
|
324
160
|
|
|
325
|
-
|
|
326
|
-
FlowChat::
|
|
161
|
+
```ruby
|
|
162
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
163
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
164
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
165
|
+
config.use_durable_sessions
|
|
166
|
+
end
|
|
327
167
|
```
|
|
328
168
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
### Basic Setup
|
|
169
|
+
One subtlety with `use_cross_platform_sessions`: dropping the `:platform` boundary only shares a session if the identifier resolves to the same value on both platforms. The platform default identifier differs (WhatsApp uses `:msisdn`, USSD uses `:request_id`), so for real cross-platform sharing pair it with a stable identifier, either `use_durable_sessions` (`:user_id`) or `identifier: :msisdn`.
|
|
332
170
|
|
|
333
|
-
|
|
334
|
-
# Enable instrumentation
|
|
335
|
-
FlowChat.setup_instrumentation!
|
|
336
|
-
```
|
|
171
|
+
## Async processing
|
|
337
172
|
|
|
338
|
-
|
|
173
|
+
Enable background processing with `use_async`. It has two forms:
|
|
339
174
|
|
|
340
175
|
```ruby
|
|
341
|
-
#
|
|
342
|
-
|
|
343
|
-
# Custom handling
|
|
344
|
-
ExternalMonitoring.track_flow_execution(
|
|
345
|
-
event.payload[:flow_name],
|
|
346
|
-
event.duration
|
|
347
|
-
)
|
|
348
|
-
end
|
|
176
|
+
# Use a factory (no custom job class needed). The factory: keyword is required.
|
|
177
|
+
config.use_async(factory: :whatsapp)
|
|
349
178
|
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
CustomLogger.log_event(name, payload.merge(duration: finish - start))
|
|
353
|
-
end
|
|
179
|
+
# Or use your own ActiveJob subclass, with optional job params.
|
|
180
|
+
config.use_async(MyFlowJob, deployment_id: 123)
|
|
354
181
|
```
|
|
355
182
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
FlowChat validates configuration at runtime and provides helpful error messages:
|
|
359
|
-
|
|
360
|
-
FlowChat validates configuration at runtime and provides helpful error messages for missing or invalid configurations.
|
|
183
|
+
The webhook enqueues the job and returns immediately; the job re-runs the flow in the background. USSD does not support async, since its protocol needs a synchronous response. See [factory-pattern.md](factory-pattern.md) and [async-background-processing.md](async-background-processing.md).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Factory pattern
|
|
2
|
+
|
|
3
|
+
A factory is a named block that builds and runs a processor. You register it once, then call it by name from both the webhook and the background job. This keeps the processor configuration in one place, so the two contexts cannot drift apart.
|
|
4
|
+
|
|
5
|
+
Without a factory, a webhook that enqueues a background job needs the same processor setup written twice: once in the controller and once in the job. A factory removes the duplication.
|
|
6
|
+
|
|
7
|
+
## Registering and executing
|
|
8
|
+
|
|
9
|
+
Register the factory in an initializer:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# config/initializers/flow_chat.rb
|
|
13
|
+
FlowChat::Factory.register(:whatsapp) do |controller|
|
|
14
|
+
processor = FlowChat::Processor.new(controller) do |config|
|
|
15
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
16
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
17
|
+
config.use_async(factory: :whatsapp)
|
|
18
|
+
end
|
|
19
|
+
processor.run(WhatsAppFlow, :start)
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Execute it from the webhook controller:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# app/controllers/whatsapp_controller.rb
|
|
27
|
+
class WhatsappController < ApplicationController
|
|
28
|
+
skip_forgery_protection
|
|
29
|
+
|
|
30
|
+
def webhook
|
|
31
|
+
FlowChat::Factory.execute(:whatsapp, controller: self)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The block receives a controller and returns whatever `processor.run` returns. Executing an unregistered name raises `FlowChat::Factory::FactoryNotFoundError`.
|
|
37
|
+
|
|
38
|
+
## How it pairs with async
|
|
39
|
+
|
|
40
|
+
Notice the factory references itself: `config.use_async(factory: :whatsapp)`. That closes the loop.
|
|
41
|
+
|
|
42
|
+
1. The webhook calls `Factory.execute(:whatsapp, controller: self)`.
|
|
43
|
+
2. The factory builds a processor with `use_async(factory: :whatsapp)`, so the gateway enqueues `FlowChat::GenericAsyncJob` with `factory: :whatsapp` and returns immediately.
|
|
44
|
+
3. The background job calls `Factory.execute(:whatsapp, controller: background_controller)` again.
|
|
45
|
+
4. This time the gateway is running in the background, so it processes the flow inline and sends the reply.
|
|
46
|
+
|
|
47
|
+
The same factory builds the processor in both passes, so there is one definition of the gateway, session store, and flow. See [async-background-processing.md](async-background-processing.md) for what happens inside the job.
|
|
48
|
+
|
|
49
|
+
## Other methods
|
|
50
|
+
|
|
51
|
+
- `FlowChat::Factory.registered?(:whatsapp)` returns whether a name is registered.
|
|
52
|
+
- `FlowChat::Factory.registered_factories` lists the registered names.
|
|
53
|
+
- `FlowChat::Factory.clear!` removes all registrations (useful in tests).
|
|
54
|
+
|
|
55
|
+
## Related
|
|
56
|
+
|
|
57
|
+
- [Async and background processing](async-background-processing.md)
|
|
58
|
+
- [Configuration](configuration.md#async-processing)
|