flow_chat 0.9.0 โ 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/.github/workflows/pages.yml +43 -0
- data/CHANGELOG.md +26 -0
- data/README.md +166 -295
- data/Rakefile +12 -1
- data/SECURITY.md +1 -1
- data/docs/architecture.md +52 -479
- data/docs/async-background-processing.md +31 -265
- data/docs/configuration.md +106 -613
- data/docs/factory-pattern.md +27 -324
- data/docs/gateway-context-variables.md +140 -143
- data/docs/gateway-development.md +86 -650
- data/docs/getting-started.md +40 -379
- data/docs/instrumentation.md +88 -279
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +47 -951
- data/docs/platforms/ussd.md +38 -653
- data/docs/platforms/whatsapp.md +73 -1321
- 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 +26 -297
- data/examples/http_controller.rb +12 -10
- data/examples/intercom_configuration_example.rb +19 -24
- data/examples/intercom_controller.rb +8 -20
- data/examples/multi_tenant_whatsapp_controller.rb +61 -166
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +86 -158
- data/examples/whatsapp_controller.rb +16 -15
- data/examples/whatsapp_media_examples.rb +27 -79
- data/lib/flow_chat/app.rb +129 -11
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +124 -1
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +24 -4
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- 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.rb +160 -1
- data/lib/flow_chat/intercom/client.rb +34 -28
- data/lib/flow_chat/intercom/configuration.rb +2 -49
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +57 -5
- 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/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +109 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +11 -2
- 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 +47 -4
- data/lib/flow_chat/telegram/configuration.rb +2 -42
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
- data/lib/flow_chat/telegram/renderer.rb +10 -2
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +14 -7
- data/lib/flow_chat/whatsapp/configuration.rb +12 -51
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
- data/lib/flow_chat/whatsapp/renderer.rb +121 -60
- data/lib/flow_chat.rb +0 -10
- 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 +51 -3
- data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
data/README.md
CHANGED
|
@@ -3,392 +3,263 @@
|
|
|
3
3
|
[](https://github.com/radioactive-labs/flow_chat/actions/workflows/ci.yml)
|
|
4
4
|
[](https://badge.fury.io/rb/flow_chat)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://rubyonrails.org/)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
**Write a conversation as an ordinary Ruby method. FlowChat runs it across stateless webhooks, on every messaging channel.**
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **๐ Unified API**: Single codebase that works across all platforms
|
|
14
|
-
- **๐ Pluggable Gateways**: Extensible architecture supporting multiple backends per platform
|
|
15
|
-
- **๐ฑ Multi-Platform**: Out of the box support for USSD, WhatsApp, Telegram, HTTP, and more, with simulator for testing
|
|
16
|
-
- **๐ฏ Screen-Based Navigation**: Intuitive screen() method for building conversational flows
|
|
17
|
-
- **๐พ Advanced Session Management**: Flexible session boundaries and storage options
|
|
18
|
-
- **๐ง Middleware Architecture**: Extensible middleware system for custom processing
|
|
19
|
-
- **๐จ Rich Prompts**: Support for text, media, selections, yes/no prompts, validation, and transformations
|
|
20
|
-
- **๐ Built-in Instrumentation**: Comprehensive logging and metrics collection
|
|
21
|
-
- **๐งช Testing Support**: Built-in simulator for development and testing
|
|
22
|
-
- **๐ข Multi-Tenancy**: In-built support for custom configuration per tenant and URL-based isolation
|
|
23
|
-
- **๐ Background Processing**: Job queue support for WhatsApp messaging
|
|
24
|
-
|
|
25
|
-
## ๐ Quick Start
|
|
26
|
-
|
|
27
|
-
### Installation
|
|
28
|
-
|
|
29
|
-
Add FlowChat to your Rails application:
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
# Gemfile
|
|
33
|
-
gem 'flow_chat'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
bundle install
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Define your flow
|
|
10
|
+
A USSD or chat webhook is stateless: each message arrives as an isolated POST with no memory of the ones before it. The usual answer is a hand-rolled state machine: persist the current step, switch on it when the next message comes in, run the transition, persist the next step, repeat. FlowChat replaces that machine with a session and a replay engine, so you write the flow as a synchronous script that reads top to bottom.
|
|
41
11
|
|
|
42
12
|
```ruby
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class WelcomeFlow < FlowChat::Flow
|
|
13
|
+
class RegistrationFlow < FlowChat::Flow
|
|
46
14
|
def main_page
|
|
47
|
-
name = app.screen(:name)
|
|
48
|
-
prompt.ask "Welcome! What's your name?",
|
|
49
|
-
transform: ->(input) { input.strip.titleize }
|
|
50
|
-
end
|
|
15
|
+
name = app.screen(:name) { |prompt| prompt.ask "What's your name?" }
|
|
51
16
|
|
|
52
|
-
|
|
53
|
-
prompt.
|
|
54
|
-
"1" => "Account Info",
|
|
55
|
-
"2" => "Make Payment",
|
|
56
|
-
"3" => "Support"
|
|
57
|
-
}
|
|
17
|
+
email = app.screen(:email) do |prompt|
|
|
18
|
+
prompt.ask "Your email?", validate: ->(input) { "Invalid email" unless input.include?("@") }
|
|
58
19
|
end
|
|
59
20
|
|
|
60
|
-
|
|
61
|
-
when "1"
|
|
62
|
-
show_account_info
|
|
63
|
-
when "2"
|
|
64
|
-
make_payment
|
|
65
|
-
when "3"
|
|
66
|
-
app.say "Call us: 123-456-7890"
|
|
67
|
-
end
|
|
21
|
+
app.say "Welcome #{name}!"
|
|
68
22
|
end
|
|
69
|
-
|
|
70
|
-
# ... implement your flow methods
|
|
71
23
|
end
|
|
72
24
|
```
|
|
73
25
|
|
|
74
|
-
|
|
26
|
+
Each `screen` returns its stored answer when one exists and re-prompts when it does not. The method runs top to bottom on every turn and blocks on the first screen that has no answer yet. The same flow runs unchanged on USSD, WhatsApp, Messenger, Instagram, Telegram, and HTTP, with per-platform rendering.
|
|
75
27
|
|
|
76
|
-
|
|
77
|
-
# app/controllers/ussd_controller.rb
|
|
78
|
-
class UssdController < ApplicationController
|
|
79
|
-
skip_forgery_protection
|
|
80
|
-
|
|
81
|
-
def process_request
|
|
82
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
83
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
84
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
85
|
-
end
|
|
28
|
+
## How the replay engine works
|
|
86
29
|
|
|
87
|
-
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
```
|
|
30
|
+
There is no saved program counter. On each webhook FlowChat rebuilds the app from the request and re-runs your flow method from the first line.
|
|
91
31
|
|
|
92
|
-
|
|
32
|
+
`app.screen(key)` is the fast-forward mechanism. When the session already holds a value for `key`, `screen` returns `session.get(key)` immediately and never yields. When it does not, `screen` yields a `FlowChat::Prompt`. If that prompt calls `ask` (or `select`, `yes?`) and no input is available yet, it raises `FlowChat::Interrupt::Prompt`. The interrupt unwinds the flow back to the Executor, which turns it into a rendered prompt and returns the response to the gateway.
|
|
93
33
|
|
|
94
|
-
|
|
95
|
-
# app/controllers/whatsapp_controller.rb
|
|
96
|
-
class WhatsappController < ApplicationController
|
|
97
|
-
skip_forgery_protection
|
|
34
|
+
The next webhook replays the same method from the top. Every screen that already has a stored answer is skipped in place. The screen that raised last time now sees the incoming input, runs its `validate` and `transform`, stores the accepted value with `session.set(key, value)`, and returns it, so execution falls through into the next screen. The flow advances one screen per turn without ever holding state between requests beyond the session hash.
|
|
98
35
|
|
|
99
|
-
|
|
100
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
101
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
102
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
103
|
-
end
|
|
36
|
+
Two rules follow from this design:
|
|
104
37
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
end
|
|
108
|
-
```
|
|
38
|
+
- One inbound message is consumed by one screen per turn. Once a screen takes the turn's input, later screens in the same run see no input and will prompt.
|
|
39
|
+
- A given screen key may be presented only once per run. Re-entering a key raises `ArgumentError`; use distinct keys or `app.go_back` to revisit.
|
|
109
40
|
|
|
110
|
-
|
|
41
|
+
## Installation
|
|
111
42
|
|
|
112
|
-
|
|
113
|
-
# app/controllers/telegram_controller.rb
|
|
114
|
-
class TelegramController < ApplicationController
|
|
115
|
-
skip_forgery_protection
|
|
116
|
-
|
|
117
|
-
def webhook
|
|
118
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
119
|
-
config.use_gateway FlowChat::Telegram::Gateway::BotApi
|
|
120
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
121
|
-
end
|
|
43
|
+
Add the gem to your Gemfile:
|
|
122
44
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
end
|
|
45
|
+
```ruby
|
|
46
|
+
gem "flow_chat"
|
|
126
47
|
```
|
|
127
48
|
|
|
128
|
-
|
|
49
|
+
Then run `bundle install`. FlowChat requires Ruby 3.0 or newer.
|
|
129
50
|
|
|
130
|
-
|
|
131
|
-
# app/controllers/api_controller.rb
|
|
132
|
-
class ApiController < ApplicationController
|
|
133
|
-
def chat
|
|
134
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
135
|
-
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
136
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
137
|
-
end
|
|
51
|
+
The cache-backed session store needs a cache. Set it once during boot, for example in an initializer:
|
|
138
52
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
end
|
|
53
|
+
```ruby
|
|
54
|
+
FlowChat::Config.cache = Rails.cache
|
|
142
55
|
```
|
|
143
|
-
> Same WelcomeFlow works across ALL platforms!
|
|
144
56
|
|
|
57
|
+
Without a cache configured, `FlowChat::Session::CacheSessionStore` raises when it tries to read or write a session. There are no migrations and no generators to run.
|
|
145
58
|
|
|
146
|
-
##
|
|
59
|
+
## Wiring a platform
|
|
147
60
|
|
|
148
|
-
|
|
61
|
+
Build a processor, choose a gateway and session store, and run your flow:
|
|
149
62
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
63
|
+
```ruby
|
|
64
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
65
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
66
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
67
|
+
end
|
|
155
68
|
|
|
156
|
-
|
|
157
|
-
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
158
|
-
โ Gateway โ -> โ Session โ -> โ Custom โ
|
|
159
|
-
โ โ โ Middleware โ โ Middleware โ
|
|
160
|
-
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
161
|
-
โ
|
|
162
|
-
v
|
|
163
|
-
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
164
|
-
โ Flow/Action โ <- โ Executor โ <- โ App โ
|
|
165
|
-
โ โ โ โ โ โ
|
|
166
|
-
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
|
|
69
|
+
processor.run RegistrationFlow, :main_page
|
|
167
70
|
```
|
|
168
71
|
|
|
169
|
-
|
|
72
|
+
Point your controller's webhook action at this code (`self` is the controller). Each platform has its own gateway class:
|
|
170
73
|
|
|
171
|
-
|
|
74
|
+
| Platform | Gateway class | Platform symbol | Rendering |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| USSD | `FlowChat::Ussd::Gateway::Nalo` | `:ussd` | Numbered text menus, pagination, media as URL text |
|
|
77
|
+
| WhatsApp | `FlowChat::Whatsapp::Gateway::CloudApi` | `:whatsapp` | Reply buttons (<=3 choices), lists (>3), rich media |
|
|
78
|
+
| Messenger | `FlowChat::Messenger::Gateway::SendApi` | `:messenger` | Quick replies, carousel, numbered text |
|
|
79
|
+
| Instagram | `FlowChat::Instagram::Gateway::SendApi` | `:instagram` | Quick replies, carousel, always numbered |
|
|
80
|
+
| Telegram | `FlowChat::Telegram::Gateway::BotApi` | `:telegram` | Inline keyboards, rich media, HTML |
|
|
81
|
+
| HTTP | `FlowChat::Http::Gateway::Simple` | `:http` | JSON responses (testing, custom clients) |
|
|
82
|
+
| Intercom | `FlowChat::Intercom::Gateway::IntercomApi` | `:intercom` | Live-chat replies |
|
|
172
83
|
|
|
173
|
-
|
|
174
|
-
# Create your own SMS gateway
|
|
175
|
-
class MyCompany::Sms::Gateway::Twilio
|
|
176
|
-
def initialize(app, config)
|
|
177
|
-
@app = app
|
|
178
|
-
@config = config
|
|
179
|
-
end
|
|
84
|
+
The same `RegistrationFlow` runs on any row by changing only `config.use_gateway`. To build a gateway for a platform not listed here, see [docs/gateway-development.md](docs/gateway-development.md).
|
|
180
85
|
|
|
181
|
-
|
|
182
|
-
# Parse Twilio webhook, set context values
|
|
183
|
-
context["request.msisdn"] = params["From"]
|
|
184
|
-
context["request.platform"] = :sms
|
|
185
|
-
context.input = params["Body"]
|
|
186
|
-
|
|
187
|
-
# Process through middleware
|
|
188
|
-
type, prompt, choices, media = @app.call(context)
|
|
189
|
-
|
|
190
|
-
# Send response via Twilio API
|
|
191
|
-
send_sms_response(prompt, to: context["request.msisdn"])
|
|
192
|
-
end
|
|
86
|
+
## Building flows
|
|
193
87
|
|
|
194
|
-
|
|
195
|
-
def self.configure_middleware_stack(builder, custom_middleware)
|
|
196
|
-
builder.use MyCompany::Sms::MessageTransformMiddleware
|
|
197
|
-
builder.use custom_middleware
|
|
198
|
-
end
|
|
199
|
-
end
|
|
88
|
+
A flow is a class that inherits `FlowChat::Flow` and reads and writes the conversation through `app`. Each unit of interaction is a screen:
|
|
200
89
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
config.use_gateway MyCompany::Sms::Gateway::Twilio, sms_config
|
|
204
|
-
end
|
|
90
|
+
```ruby
|
|
91
|
+
value = app.screen(:key) { |prompt| prompt.ask "..." }
|
|
205
92
|
```
|
|
206
93
|
|
|
207
|
-
|
|
94
|
+
The block receives a `FlowChat::Prompt`. Its methods:
|
|
208
95
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
96
|
+
| Method | Signature | Behavior |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `ask` | `ask(msg, choices: nil, transform: nil, validate: nil, media: nil)` | Prompt for free input; validate, then transform, then return the value |
|
|
99
|
+
| `select` | `select(msg, choices, media: nil, error_message: "Invalid selection:")` | Prompt for one of `choices`; returns the chosen key |
|
|
100
|
+
| `yes?` | `yes?(msg)` | A `select` over Yes/No; returns `true` or `false` |
|
|
101
|
+
| `say` | `say(msg, media: nil)` | Send a terminal message and end the flow |
|
|
213
102
|
|
|
214
|
-
|
|
215
|
-
- [**USSD Development**](docs/platforms/ussd.md) - USSD-specific features and examples
|
|
216
|
-
- [**WhatsApp Development**](docs/platforms/whatsapp.md) - WhatsApp Business API integration
|
|
217
|
-
- [**Telegram Development**](docs/platforms/telegram.md) - Telegram Bot API integration
|
|
218
|
-
- [**HTTP Development**](docs/platforms/http.md) ๐ง - API endpoints and webhooks
|
|
219
|
-
- [**Multi-Platform Apps**](docs/platforms/multi-platform.md) ๐ง - Building unified experiences
|
|
103
|
+
Details:
|
|
220
104
|
|
|
221
|
-
|
|
222
|
-
-
|
|
223
|
-
- [
|
|
224
|
-
-
|
|
225
|
-
- [**Testing & Simulation**](docs/testing.md) - Testing strategies and simulator usage
|
|
226
|
-
- [**Background Jobs**](docs/background-jobs.md) ๐ง - Async processing for WhatsApp
|
|
227
|
-
- [**Instrumentation**](docs/instrumentation.md) - Monitoring, logging, and error tracking
|
|
105
|
+
- `validate` is a callable that returns an error string when the input is rejected and `nil` when it passes. The received argument behaves like the input text (`input.include?("@")`, `input.to_i`, `input.strip`), and also exposes `media`, `location`, and `contact` for attachment-carrying turns.
|
|
106
|
+
- `transform` is a callable that maps the accepted input to the value `screen` stores and returns.
|
|
107
|
+
- `choices` may be an Array (`["Yes", "No"]`) or a Hash (`{ "1" => "Account", "2" => "Support" }`). With a Hash, `select` returns the key.
|
|
108
|
+
- Passing `media:` together with more than 3 `choices` raises `ArgumentError`. Use media or a longer choice list, not both.
|
|
228
109
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
110
|
+
`app.say` outside a block ends the flow from anywhere:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
def main_page
|
|
114
|
+
confirmed = app.screen(:confirm) { |prompt| prompt.yes? "Place the order?" }
|
|
115
|
+
app.say("Cancelled.") unless confirmed
|
|
116
|
+
# ...
|
|
117
|
+
end
|
|
118
|
+
```
|
|
234
119
|
|
|
235
|
-
|
|
120
|
+
To send the user back to the previous screen, call `app.go_back`. It clears the current screen's stored answer and restarts the flow from the top, so the earlier screen prompts again. Remember that a screen key may appear only once per run: reuse of a key in a single pass raises `ArgumentError`.
|
|
236
121
|
|
|
237
|
-
|
|
122
|
+
## Media and rich input
|
|
238
123
|
|
|
239
|
-
|
|
124
|
+
An inbound turn is a `FlowChat::Input`. Read what the user sent through these accessors on `app`:
|
|
240
125
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
end
|
|
126
|
+
| Accessor | Returns |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `app.text` | The message text (`""` when the turn carries only an attachment) |
|
|
129
|
+
| `app.media` | An Array of `FlowChat::Media` (empty array when none) |
|
|
130
|
+
| `app.location` | The shared location payload, or `nil` |
|
|
131
|
+
| `app.contact` | The shared contact card, or `nil` |
|
|
132
|
+
| `app.attachment_type` | `:media`, `:location`, `:contact`, or `nil` |
|
|
250
133
|
|
|
251
|
-
|
|
252
|
-
prompt.ask "Enter your full name:",
|
|
253
|
-
transform: ->(input) { input.strip.titleize }
|
|
254
|
-
end
|
|
134
|
+
Reading an inbound photo:
|
|
255
135
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
136
|
+
```ruby
|
|
137
|
+
def main_page
|
|
138
|
+
app.screen(:photo) do |prompt|
|
|
139
|
+
prompt.ask "Send a photo of the receipt."
|
|
259
140
|
end
|
|
260
141
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
142
|
+
photo = app.media.first
|
|
143
|
+
if photo
|
|
144
|
+
photo.type # => :image (a Symbol; normalized across platforms)
|
|
145
|
+
photo.mime_type # => "image/jpeg"
|
|
146
|
+
photo.caption # => the caption text, or nil
|
|
147
|
+
bytes = photo.download # raw bytes, or nil on failure
|
|
148
|
+
link = photo.url # a fetchable URL, or nil
|
|
266
149
|
end
|
|
267
150
|
end
|
|
268
151
|
```
|
|
269
152
|
|
|
270
|
-
|
|
153
|
+
`app.media` is always an Array, so read a single item with `app.media.first` and check for `nil`; the type is a Symbol, read with `app.media.first&.type`. A caption-less photo still answers a screen: an attachment counts as submitted even when the text is blank.
|
|
271
154
|
|
|
272
|
-
|
|
155
|
+
To send media outbound, pass `media:` to `ask` or `say`:
|
|
273
156
|
|
|
274
157
|
```ruby
|
|
275
|
-
|
|
276
|
-
def main_menu
|
|
277
|
-
choice = app.screen(:menu) do |prompt|
|
|
278
|
-
prompt.select "Choose an option:", {
|
|
279
|
-
"info" => "๐ Information", # Rich for WhatsApp
|
|
280
|
-
"help" => "โ Help", # Falls back to text for USSD
|
|
281
|
-
"exit" => "๐ Exit"
|
|
282
|
-
}
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
# Same logic works on both platforms
|
|
286
|
-
handle_choice(choice)
|
|
287
|
-
end
|
|
288
|
-
end
|
|
158
|
+
app.say "Here you go", media: { type: :image, url: "https://example.com/receipt.png" }
|
|
289
159
|
```
|
|
290
160
|
|
|
291
|
-
|
|
161
|
+
Caveat: a `Media` object that has been deserialized from a session store or a background job has lost its live platform client, so `url` and `download` return `nil`. If you need the bytes, call `download` eagerly in the same request that received the media, before the value round-trips through a session or job.
|
|
292
162
|
|
|
293
|
-
|
|
163
|
+
## Sessions
|
|
294
164
|
|
|
295
|
-
|
|
296
|
-
# Durable sessions across timeouts
|
|
297
|
-
processor = FlowChat::Processor.new(self) do |config|
|
|
298
|
-
config.use_durable_sessions # Uses user identifier (usually phone number) for session ID
|
|
299
|
-
end
|
|
165
|
+
The default session boundaries are `[:flow, :gateway, :platform]`: a session is scoped to one flow, on one gateway, on one platform. Convenience methods adjust this:
|
|
300
166
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
167
|
+
| Method | Effect |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `use_durable_sessions` | Key the session on a stable per-user identifier so it survives USSD session-id rotation across a conversation |
|
|
170
|
+
| `use_cross_platform_sessions` | Narrow the boundary to flow only, so one session is shared across platforms (for example USSD and WhatsApp) |
|
|
171
|
+
| `use_url_isolation` | Add a `:url` boundary for per-tenant or per-host isolation |
|
|
172
|
+
| `use_session_config(boundaries:, identifier:, hash_identifiers:, &block)` | Set boundaries, the identifier, and identifier hashing directly for full control |
|
|
305
173
|
|
|
306
|
-
|
|
174
|
+
```ruby
|
|
307
175
|
processor = FlowChat::Processor.new(self) do |config|
|
|
308
|
-
config.
|
|
176
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
177
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
178
|
+
config.use_durable_sessions
|
|
309
179
|
end
|
|
310
180
|
```
|
|
311
181
|
|
|
312
|
-
|
|
182
|
+
See [docs/configuration.md](docs/configuration.md) for the full set of session options.
|
|
313
183
|
|
|
314
|
-
|
|
315
|
-
|----------|-------------------|----------|
|
|
316
|
-
| **USSD** | `Nalo` โ
, Custom | Pagination, choice mapping, session management |
|
|
317
|
-
| **WhatsApp** | `CloudApi` โ
, Custom | Rich media, buttons, lists, templates, background jobs |
|
|
318
|
-
| **Telegram** | `BotApi` โ
, Custom | Inline keyboards, rich media, callbacks, group chats |
|
|
319
|
-
| **HTTP** | `Simple` โ
, Custom | Testing, webhooks, API endpoints, JSON responses |
|
|
320
|
-
| **Simulator** | Built-in โ
| Development testing, conversation replay, flow debugging |
|
|
321
|
-
| **Custom** | *Your Gateway* | Implement any platform by creating a gateway class |
|
|
184
|
+
## Platform differences
|
|
322
185
|
|
|
323
|
-
|
|
186
|
+
The flow code is the same everywhere, but each platform imposes limits that the rendering respects and that you should keep in mind:
|
|
324
187
|
|
|
325
|
-
|
|
188
|
+
| Platform | Limits and behavior |
|
|
189
|
+
|---|---|
|
|
190
|
+
| USSD | Pages are split at 140 characters by default; media is rendered as a text line containing a URL, not an inline attachment; async processing is not available. |
|
|
191
|
+
| WhatsApp | Reply-button titles are truncated near 20 characters and list titles near 24; a list section holds at most 10 rows. WhatsApp's 24-hour customer-service window and its template requirement are not abstracted away: you manage message templates yourself. |
|
|
192
|
+
| Messenger | Up to 13 choices render as quick replies, up to 30 as a carousel (10 elements, 3 buttons each), and above that as numbered text. Neither platform abstracts the 24-hour messaging window; a send outside it is attempted and Meta's rejection surfaces as an API error. |
|
|
193
|
+
| Instagram | Same quick-reply and carousel caps as Messenger (13, then 30), but both surfaces render on the mobile app only, not desktop or web, so Instagram always lists the options as numbered text as well. Text is capped at 1000 bytes UTF-8, not characters. |
|
|
194
|
+
| Telegram | Choice taps arrive as callback queries; choices render as inline keyboards; message text supports HTML formatting. |
|
|
195
|
+
| HTTP | Requests and responses are JSON; each request must supply `session_id` and `user_id`. |
|
|
326
196
|
|
|
327
|
-
|
|
328
|
-
# Built-in gateways (included with FlowChat)
|
|
329
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
330
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
331
|
-
config.use_gateway FlowChat::Telegram::Gateway::BotApi, telegram_config
|
|
332
|
-
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
333
|
-
|
|
334
|
-
# Custom gateway examples (you would build these)
|
|
335
|
-
config.use_gateway MyCompany::Sms::Gateway::Twilio, twilio_config
|
|
336
|
-
config.use_gateway MyCompany::Slack::Gateway::BoltJS, slack_config
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
## ๐ฆ Example Applications
|
|
197
|
+
Platform guides: [docs/platforms/ussd.md](docs/platforms/ussd.md), [docs/platforms/whatsapp.md](docs/platforms/whatsapp.md), [docs/platforms/messenger.md](docs/platforms/messenger.md), [docs/platforms/instagram.md](docs/platforms/instagram.md), [docs/platforms/telegram.md](docs/platforms/telegram.md).
|
|
340
198
|
|
|
341
|
-
|
|
342
|
-
- [**WhatsApp Customer Service**](docs/examples/whatsapp-support.md) ๐ง - Media support and templates
|
|
343
|
-
- [**HTTP API Chatbot**](docs/examples/http-api.md) ๐ง - JSON API for web/mobile integration
|
|
344
|
-
- [**Multi-Platform E-commerce**](docs/examples/multi-platform-shop.md) ๐ง - Unified shopping across USSD, WhatsApp, and HTTP
|
|
345
|
-
- [**Multi-Tenant SaaS**](docs/examples/multi-tenant.md) ๐ง - URL-based tenant isolation
|
|
346
|
-
- [**Custom Gateway Example**](docs/examples/custom-gateway.md) ๐ง - Building a Telegram gateway
|
|
199
|
+
## Background processing
|
|
347
200
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
FlowChat includes a built-in simulator interface for easy development and testing:
|
|
201
|
+
For platforms with an outbound API, you can acknowledge the webhook immediately and run the flow in a background job. Register a factory once, then call it from the webhook:
|
|
351
202
|
|
|
352
203
|
```ruby
|
|
353
|
-
#
|
|
354
|
-
|
|
355
|
-
|
|
204
|
+
# config/initializers/flow_chat.rb
|
|
205
|
+
FlowChat::Factory.register(:whatsapp) do |controller|
|
|
206
|
+
processor = FlowChat::Processor.new(controller) do |config|
|
|
207
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
208
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
209
|
+
config.use_async(factory: :whatsapp)
|
|
210
|
+
end
|
|
211
|
+
processor.run RegistrationFlow, :main_page
|
|
356
212
|
end
|
|
357
213
|
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
214
|
+
# app/controllers/whatsapp_controller.rb
|
|
215
|
+
def webhook
|
|
216
|
+
FlowChat::Factory.execute(:whatsapp, controller: self)
|
|
361
217
|
end
|
|
362
218
|
```
|
|
363
219
|
|
|
364
|
-
The
|
|
220
|
+
The webhook enqueues `GenericAsyncJob` and returns; the job re-runs the same factory in the background, where the gateway detects the background context and processes the flow inline before sending the reply through the platform API. USSD does not support async, since it depends on a synchronous request-response cycle. See [docs/factory-pattern.md](docs/factory-pattern.md) and [docs/async-background-processing.md](docs/async-background-processing.md).
|
|
365
221
|
|
|
366
|
-
|
|
222
|
+
## Instrumentation
|
|
367
223
|
|
|
368
|
-
|
|
224
|
+
FlowChat emits `ActiveSupport::Notifications` events for flow execution, session lifecycle, and gateway activity. `FlowChat.metrics` collects counters over those events. See [docs/instrumentation.md](docs/instrumentation.md).
|
|
369
225
|
|
|
370
|
-
|
|
226
|
+
## Testing
|
|
371
227
|
|
|
372
|
-
|
|
373
|
-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
374
|
-
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
375
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
376
|
-
5. Open a Pull Request
|
|
228
|
+
FlowChat ships a web simulator for driving flows locally without a real gateway, useful during development. It requires `FlowChat::Config.simulator_secret` to be set. See [docs/testing.md](docs/testing.md).
|
|
377
229
|
|
|
378
|
-
##
|
|
230
|
+
## Documentation
|
|
379
231
|
|
|
380
|
-
|
|
232
|
+
Guides:
|
|
233
|
+
|
|
234
|
+
- [Getting started](docs/getting-started.md)
|
|
235
|
+
- [Configuration](docs/configuration.md)
|
|
236
|
+
- [Testing](docs/testing.md)
|
|
237
|
+
|
|
238
|
+
Platforms:
|
|
381
239
|
|
|
382
|
-
|
|
240
|
+
- [USSD](docs/platforms/ussd.md)
|
|
241
|
+
- [WhatsApp](docs/platforms/whatsapp.md)
|
|
242
|
+
- [Messenger](docs/platforms/messenger.md)
|
|
243
|
+
- [Instagram](docs/platforms/instagram.md)
|
|
244
|
+
- [Telegram](docs/platforms/telegram.md)
|
|
383
245
|
|
|
384
|
-
|
|
385
|
-
- **Issues**: [GitHub Issues](https://github.com/radioactive-labs/flow_chat/issues)
|
|
386
|
-
- **Discussions**: [GitHub Discussions](https://github.com/radioactive-labs/flow_chat/discussions)
|
|
246
|
+
Internals and advanced:
|
|
387
247
|
|
|
388
|
-
|
|
248
|
+
- [Architecture](docs/architecture.md)
|
|
249
|
+
- [Building a gateway](docs/gateway-development.md)
|
|
250
|
+
- [Gateway context variables](docs/gateway-context-variables.md)
|
|
251
|
+
- [Async and background processing](docs/async-background-processing.md)
|
|
252
|
+
- [Factory pattern](docs/factory-pattern.md)
|
|
253
|
+
- [Instrumentation](docs/instrumentation.md)
|
|
389
254
|
|
|
390
|
-
|
|
255
|
+
## Development
|
|
391
256
|
|
|
392
|
-
|
|
257
|
+
Run the test suite with `rake test`. To run a single file, use `ruby -Itest test/unit/some_test.rb`.
|
|
393
258
|
|
|
394
|
-
|
|
259
|
+
## Contributing
|
|
260
|
+
|
|
261
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/radioactive-labs/flow_chat). Please add tests for behavior changes and keep them passing.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
FlowChat is released under the [MIT License](LICENSE.txt).
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,18 @@ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
|
|
|
8
8
|
Rake::TestTask.new(:test) do |t|
|
|
9
9
|
t.libs << "test"
|
|
10
10
|
t.libs << "lib"
|
|
11
|
-
|
|
11
|
+
# test/performance holds benchmarks, which assert against wall-clock timings and
|
|
12
|
+
# so fail on a loaded machine for reasons that have nothing to do with the code
|
|
13
|
+
# under test. A suite that cries wolf is a suite people stop reading, so they
|
|
14
|
+
# run under `rake benchmark` instead of here.
|
|
15
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/performance/**/*_test.rb")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Run the performance benchmarks (timing-sensitive, not part of rake test)"
|
|
19
|
+
Rake::TestTask.new(:benchmark) do |t|
|
|
20
|
+
t.libs << "test"
|
|
21
|
+
t.libs << "lib"
|
|
22
|
+
t.test_files = FileList["test/performance/**/*_test.rb"]
|
|
12
23
|
end
|
|
13
24
|
|
|
14
25
|
task default: %i[test standard]
|
data/SECURITY.md
CHANGED
|
@@ -13,7 +13,7 @@ please upgrade before reporting an issue to confirm it still reproduces.
|
|
|
13
13
|
|
|
14
14
|
| Version | Supported |
|
|
15
15
|
| ------- | ------------------ |
|
|
16
|
-
| Latest release (`0.
|
|
16
|
+
| Latest release (`0.10.x`) | :white_check_mark: |
|
|
17
17
|
| Older releases | :x: |
|
|
18
18
|
|
|
19
19
|
## Reporting a Vulnerability
|