flow_chat 0.8.2 → 0.9.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/release.yml +56 -0
  5. data/.standard.yml +4 -0
  6. data/CHANGELOG.md +22 -0
  7. data/CLAUDE.md +327 -0
  8. data/CONTRIBUTING.md +134 -0
  9. data/Gemfile +1 -0
  10. data/README.md +290 -105
  11. data/Rakefile +5 -1
  12. data/SECURITY.md +42 -349
  13. data/docs/architecture.md +510 -0
  14. data/docs/async-background-processing.md +298 -0
  15. data/docs/configuration.md +556 -226
  16. data/docs/factory-pattern.md +355 -0
  17. data/docs/gateway-context-variables.md +171 -0
  18. data/docs/gateway-development.md +723 -0
  19. data/docs/getting-started.md +429 -0
  20. data/docs/instrumentation.md +264 -153
  21. data/docs/platforms/telegram.md +1013 -0
  22. data/docs/platforms/ussd.md +693 -0
  23. data/docs/platforms/whatsapp.md +1395 -0
  24. data/docs/testing.md +243 -365
  25. data/examples/custom_session_id_example.rb +119 -0
  26. data/examples/http_controller.rb +11 -11
  27. data/examples/intercom_configuration_example.rb +118 -0
  28. data/examples/intercom_controller.rb +194 -0
  29. data/examples/multi_tenant_whatsapp_controller.rb +4 -4
  30. data/examples/ussd_controller.rb +9 -9
  31. data/examples/whatsapp_controller.rb +2 -2
  32. data/flow_chat.gemspec +4 -0
  33. data/lib/flow_chat/{base_app.rb → app.rb} +16 -9
  34. data/lib/flow_chat/async_job.rb +176 -0
  35. data/lib/flow_chat/config.rb +10 -30
  36. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  37. data/lib/flow_chat/factory.rb +94 -0
  38. data/lib/flow_chat/gateway_async_support.rb +106 -0
  39. data/lib/flow_chat/generic_async_job.rb +30 -0
  40. data/lib/flow_chat/http/gateway/simple.rb +81 -33
  41. data/lib/flow_chat/http/renderer.rb +3 -3
  42. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  43. data/lib/flow_chat/instrumentation.rb +23 -0
  44. data/lib/flow_chat/intercom/client.rb +155 -0
  45. data/lib/flow_chat/intercom/configuration.rb +149 -0
  46. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  47. data/lib/flow_chat/intercom/renderer.rb +71 -0
  48. data/lib/flow_chat/phone_number_util.rb +37 -35
  49. data/lib/flow_chat/processor.rb +188 -0
  50. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  51. data/lib/flow_chat/session/middleware.rb +25 -9
  52. data/lib/flow_chat/telegram/client.rb +240 -0
  53. data/lib/flow_chat/telegram/configuration.rb +118 -0
  54. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  55. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  56. data/lib/flow_chat/telegram/renderer.rb +125 -0
  57. data/lib/flow_chat/telegram.rb +7 -0
  58. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  59. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  60. data/lib/flow_chat/ussd/renderer.rb +1 -1
  61. data/lib/flow_chat/version.rb +1 -1
  62. data/lib/flow_chat/whatsapp/client.rb +144 -13
  63. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  64. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +132 -96
  65. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  66. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  67. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  68. data/lib/flow_chat.rb +11 -1
  69. data/lib/tasks/release.rake +165 -0
  70. metadata +84 -25
  71. data/docs/flows.md +0 -320
  72. data/docs/http-gateway-protocol.md +0 -432
  73. data/docs/images/simulator.png +0 -0
  74. data/docs/media.md +0 -153
  75. data/docs/sessions.md +0 -433
  76. data/docs/ussd-setup.md +0 -322
  77. data/docs/whatsapp-setup.md +0 -162
  78. data/examples/whatsapp_message_job.rb +0 -113
  79. data/lib/flow_chat/base_processor.rb +0 -146
  80. data/lib/flow_chat/http/app.rb +0 -6
  81. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  82. data/lib/flow_chat/http/processor.rb +0 -33
  83. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  84. data/lib/flow_chat/ussd/app.rb +0 -6
  85. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  86. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  87. data/lib/flow_chat/ussd/processor.rb +0 -39
  88. data/lib/flow_chat/whatsapp/app.rb +0 -29
  89. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  90. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  91. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -0,0 +1,355 @@
1
+ # Factory Pattern
2
+
3
+ The Factory pattern provides centralized processor configuration, eliminating duplication between webhook controllers and background jobs.
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Register Factories
8
+
9
+ Register your processor configurations once in an initializer:
10
+
11
+ ```ruby
12
+ # config/initializers/flow_chat.rb
13
+
14
+ FlowChat::Factory.register :whatsapp do |controller|
15
+ processor = FlowChat::Processor.new(controller) do |config|
16
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
17
+ config.use_session_store FlowChat::Session::CacheSessionStore
18
+ config.use_session_config(boundaries: [:flow])
19
+ end
20
+ processor.run(WhatsAppFlow, :start)
21
+ end
22
+
23
+ FlowChat::Factory.register :intercom do |controller|
24
+ processor = FlowChat::Processor.new(controller) do |config|
25
+ config.use_gateway FlowChat::Intercom::Gateway::IntercomApi
26
+ config.use_session_store FlowChat::Session::CacheSessionStore
27
+ config.use_session_config(boundaries: [:conversation], identifier: :conversation_id)
28
+ end
29
+ processor.run(IntercomFlow, :start)
30
+ end
31
+ ```
32
+
33
+ ### 2. Use in Controllers
34
+
35
+ Execute factories directly in webhook controllers:
36
+
37
+ ```ruby
38
+ # app/controllers/webhooks_controller.rb
39
+ class WebhooksController < ApplicationController
40
+ def whatsapp
41
+ FlowChat::Factory.execute(:whatsapp, controller: self)
42
+ end
43
+
44
+ def intercom
45
+ FlowChat::Factory.execute(:intercom, controller: self)
46
+ end
47
+ end
48
+ ```
49
+
50
+ ### 3. Use with Async (Recommended)
51
+
52
+ Register factory with async support for background processing:
53
+
54
+ ```ruby
55
+ # config/initializers/flow_chat.rb
56
+ FlowChat::Factory.register :whatsapp do |controller|
57
+ processor = FlowChat::Processor.new(controller) do |config|
58
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
59
+ config.use_session_store FlowChat::Session::CacheSessionStore
60
+ config.use_session_config(boundaries: [:flow])
61
+ config.use_async(factory: :whatsapp) # Self-referencing for async
62
+ end
63
+ processor.run(WhatsAppFlow, :start)
64
+ end
65
+
66
+ # app/controllers/webhooks_controller.rb
67
+ class WebhooksController < ApplicationController
68
+ def whatsapp
69
+ FlowChat::Factory.execute(:whatsapp, controller: self)
70
+ end
71
+ end
72
+ ```
73
+
74
+ **How it works:**
75
+ 1. Webhook receives request
76
+ 2. `use_async(factory: :whatsapp)` automatically uses `GenericAsyncJob`
77
+ 3. Job is enqueued with `factory: :whatsapp` parameter
78
+ 4. Background worker executes `FlowChat::Factory.execute(:whatsapp, controller: background_controller)`
79
+ 5. Flow processes in background with same configuration
80
+
81
+ ## Benefits
82
+
83
+ ### 1. Single Source of Truth
84
+
85
+ Define processor configuration once, use everywhere:
86
+
87
+ ```ruby
88
+ # Before: Duplicated configuration
89
+ # Webhook controller
90
+ processor = FlowChat::Processor.new(self) do |config|
91
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
92
+ config.use_session_store FlowChat::Session::CacheSessionStore
93
+ config.use_session_config(boundaries: [:flow])
94
+ end
95
+
96
+ # Background job (duplicate!)
97
+ processor = FlowChat::Processor.new(controller) do |config|
98
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
99
+ config.use_session_store FlowChat::Session::CacheSessionStore
100
+ config.use_session_config(boundaries: [:flow])
101
+ end
102
+
103
+ # After: Single registration
104
+ FlowChat::Factory.register :whatsapp do |controller|
105
+ processor = FlowChat::Processor.new(controller) do |config|
106
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
107
+ config.use_session_store FlowChat::Session::CacheSessionStore
108
+ config.use_session_config(boundaries: [:flow])
109
+ end
110
+ processor.run(WhatsAppFlow, :start)
111
+ end
112
+
113
+ # Use everywhere with one line
114
+ FlowChat::Factory.execute(:whatsapp, controller: self)
115
+ ```
116
+
117
+ ### 2. No Custom Job Classes
118
+
119
+ For simple cases, use `GenericAsyncJob` automatically:
120
+
121
+ ```ruby
122
+ # No need to create MyFlowJob class
123
+ config.use_async(factory: :whatsapp)
124
+ ```
125
+
126
+ ### 3. Context-Aware Execution
127
+
128
+ Same factory works in both webhook and background contexts:
129
+
130
+ ```ruby
131
+ # Webhook context - processes inline or enqueues
132
+ FlowChat::Factory.execute(:whatsapp, controller: webhook_controller)
133
+
134
+ # Background context - always processes inline
135
+ FlowChat::Factory.execute(:whatsapp, controller: background_controller)
136
+ ```
137
+
138
+ Gateways automatically detect the context and prevent double-enqueueing.
139
+
140
+ ### 4. Easy Testing
141
+
142
+ Test factories in isolation:
143
+
144
+ ```ruby
145
+ RSpec.describe "WhatsApp Factory" do
146
+ it "executes successfully" do
147
+ controller = mock_controller
148
+ expect {
149
+ FlowChat::Factory.execute(:whatsapp, controller: controller)
150
+ }.not_to raise_error
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## API Reference
156
+
157
+ ### Registration
158
+
159
+ **`FlowChat::Factory.register(name, &block)`**
160
+
161
+ Register a factory with a given name.
162
+
163
+ ```ruby
164
+ FlowChat::Factory.register :my_flow do |controller|
165
+ # Build and run processor
166
+ processor = FlowChat::Processor.new(controller) do |config|
167
+ # ... configuration
168
+ end
169
+ processor.run(MyFlow, :start)
170
+ end
171
+ ```
172
+
173
+ **Parameters:**
174
+ - `name` (Symbol): Unique factory identifier
175
+ - `block` (Proc): Factory block receiving `controller`
176
+
177
+ **Returns:** `nil`
178
+
179
+ ### Execution
180
+
181
+ **`FlowChat::Factory.execute(name, controller:)`**
182
+
183
+ Execute a registered factory.
184
+
185
+ ```ruby
186
+ FlowChat::Factory.execute(:my_flow, controller: self)
187
+ ```
188
+
189
+ **Parameters:**
190
+ - `name` (Symbol): Factory name to execute
191
+ - `controller` (Object): Rails controller or BackgroundController
192
+
193
+ **Returns:** Result of factory block
194
+
195
+ **Raises:**
196
+ - `FlowChat::Factory::FactoryNotFoundError` if factory not registered
197
+
198
+ ### Introspection
199
+
200
+ **`FlowChat::Factory.registered?(name)`**
201
+
202
+ Check if factory is registered.
203
+
204
+ ```ruby
205
+ FlowChat::Factory.registered?(:whatsapp) # => true
206
+ ```
207
+
208
+ **`FlowChat::Factory.registered_factories`**
209
+
210
+ Get all registered factory names.
211
+
212
+ ```ruby
213
+ FlowChat::Factory.registered_factories # => [:whatsapp, :intercom, :ussd]
214
+ ```
215
+
216
+ **`FlowChat::Factory.clear!`**
217
+
218
+ Clear all registered factories (primarily for testing).
219
+
220
+ ```ruby
221
+ FlowChat::Factory.clear!
222
+ ```
223
+
224
+ ## Advanced Patterns
225
+
226
+ ### Environment-Specific Factories
227
+
228
+ Create different configurations per environment:
229
+
230
+ ```ruby
231
+ if Rails.env.production?
232
+ FlowChat::Factory.register :whatsapp do |controller|
233
+ processor = FlowChat::Processor.new(controller) do |config|
234
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
235
+ config.use_session_store FlowChat::Session::CacheSessionStore
236
+ end
237
+ processor.run(ProductionFlow, :start)
238
+ end
239
+ else
240
+ FlowChat::Factory.register :whatsapp do |controller|
241
+ processor = FlowChat::Processor.new(controller) do |config|
242
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
243
+ config.use_session_store FlowChat::Session::RailsSessionStore
244
+ end
245
+ processor.run(DevelopmentFlow, :start)
246
+ end
247
+ end
248
+ ```
249
+
250
+ ### Multiple Variants
251
+
252
+ Register multiple factories for the same platform:
253
+
254
+ ```ruby
255
+ FlowChat::Factory.register :whatsapp_support do |controller|
256
+ processor = FlowChat::Processor.new(controller) do |config|
257
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
258
+ config.use_session_store FlowChat::Session::CacheSessionStore
259
+ end
260
+ processor.run(SupportFlow, :start)
261
+ end
262
+
263
+ FlowChat::Factory.register :whatsapp_sales do |controller|
264
+ processor = FlowChat::Processor.new(controller) do |config|
265
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
266
+ config.use_session_store FlowChat::Session::CacheSessionStore
267
+ end
268
+ processor.run(SalesFlow, :start)
269
+ end
270
+
271
+ # Use based on routing
272
+ def whatsapp
273
+ factory = params[:department] == 'sales' ? :whatsapp_sales : :whatsapp_support
274
+ FlowChat::Factory.execute(factory, controller: self)
275
+ end
276
+ ```
277
+
278
+ ### Accessing Request Data
279
+
280
+ Factories receive the controller, which includes request data:
281
+
282
+ ```ruby
283
+ FlowChat::Factory.register :whatsapp do |controller|
284
+ # Access request params
285
+ user_id = controller.params[:user_id]
286
+ platform = controller.request.headers["User-Agent"]
287
+
288
+ processor = FlowChat::Processor.new(controller) do |config|
289
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
290
+ config.use_session_store FlowChat::Session::CacheSessionStore
291
+ end
292
+
293
+ # Route to different flows based on params
294
+ flow = user_id.start_with?('premium_') ? PremiumFlow : StandardFlow
295
+ processor.run(flow, :start)
296
+ end
297
+ ```
298
+
299
+ ## Migration Guide
300
+
301
+ ### From Duplicate Configuration
302
+
303
+ **Before:**
304
+ ```ruby
305
+ # Webhook controller
306
+ class WebhooksController < ApplicationController
307
+ def whatsapp
308
+ processor = FlowChat::Processor.new(self) do |config|
309
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
310
+ config.use_session_store FlowChat::Session::CacheSessionStore
311
+ config.use_async(WhatsAppFlowJob)
312
+ end
313
+ processor.run(WhatsAppFlow, :start)
314
+ end
315
+ end
316
+
317
+ # Background job (duplicate!)
318
+ class WhatsAppFlowJob < FlowChat::AsyncJob
319
+ def execute(controller, **job_params)
320
+ processor = FlowChat::Processor.new(controller) do |config|
321
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
322
+ config.use_session_store FlowChat::Session::CacheSessionStore
323
+ end
324
+ processor.run(WhatsAppFlow, :start)
325
+ end
326
+ end
327
+ ```
328
+
329
+ **After:**
330
+ ```ruby
331
+ # config/initializers/flow_chat.rb
332
+ FlowChat::Factory.register :whatsapp do |controller|
333
+ processor = FlowChat::Processor.new(controller) do |config|
334
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
335
+ config.use_session_store FlowChat::Session::CacheSessionStore
336
+ config.use_async(factory: :whatsapp) # Self-referencing for async
337
+ end
338
+ processor.run(WhatsAppFlow, :start)
339
+ end
340
+
341
+ # Webhook controller - ONE LINE!
342
+ class WebhooksController < ApplicationController
343
+ def whatsapp
344
+ FlowChat::Factory.execute(:whatsapp, controller: self)
345
+ end
346
+ end
347
+
348
+ # No custom job class needed!
349
+ ```
350
+
351
+ ## See Also
352
+
353
+ - [Async Background Processing](async-background-processing.md) - Using factories with async jobs
354
+ - [Session Management](session-management.md) - Configuring session stores in factories
355
+ - [Gateways](gateways.md) - Platform-specific gateway configuration
@@ -0,0 +1,171 @@
1
+ # Gateway Context Variables
2
+
3
+ This document describes all context variables set by each gateway in FlowChat.
4
+
5
+ ## All Context Variables
6
+
7
+ | Variable | USSD Nalo | HTTP Simple | WhatsApp Cloud API | Telegram Bot API | Intercom API | Description |
8
+ |----------|-----------|-------------|-------------------|------------------|--------------|-------------|
9
+ | **Common Variables** |
10
+ | `request.id` | ✓ Session ID | ✓ From user_params | ✓ Phone number | ✓ Chat ID | ✓ Conversation ID | Unique identifier for the session/conversation |
11
+ | `request.user_id` | ✓ = msisdn | ✓ From user_params | ✓ Phone number | ✓ Telegram user ID | ✓ Contact ID | User/contact identifier |
12
+ | `request.user_name` | ✗ | ✓ (optional) | ✓ (optional) | ✓ First + Last name | ✓ (optional) | User's display name |
13
+ | `request.username` | ✗ | ✗ | ✗ | ✓ Telegram username | ✗ | Telegram @username |
14
+ | `request.msisdn` | ✓ | ✓ (optional) | ✓ | ✗ | ✓ (optional) | E.164 phone number |
15
+ | `request.email` | ✗ | ✓ (optional) | ✗ | ✗ | ✓ (optional) | User email |
16
+ | `request.message_id` | ✓ UUID | ✓ UUID | ✓ WhatsApp ID | ✓ Telegram msg ID | ✓ (optional) | Message identifier |
17
+ | `request.timestamp` | ✓ Current | ✓ Current | ✓ Current | ✓ From message | ✓ Current | ISO8601 timestamp |
18
+ | `request.gateway` | ✓ `:nalo` | ✓ `:http_simple` | ✓ `:whatsapp_cloud_api` | ✓ `:telegram_bot_api` | ✓ `:intercom_api` | Gateway name |
19
+ | `request.platform` | ✓ `:ussd` | ✓ `:http` | ✓ `:whatsapp` | ✓ `:telegram` | ✓ `:intercom` | Platform type |
20
+ | `request.body` | ✓ | ✓ | ✓ | ✓ | ✓ | Raw request body (stringified keys) |
21
+ | `request.input` | ✓ Text | ✓ Text | ✓ Varies⁴ | ✓ Varies⁶ | ✓ Text/nil⁵ | User's input message |
22
+ | **WhatsApp-Specific** |
23
+ | `request.location` | ✗ | ✗ | ✓ | ✓ | ✗ | Location data (when input is `"$location$"`) |
24
+ | `request.media` | ✗ | ✗ | ✓ | ✓ | ✗ | Media metadata (when input is `"$media$"`) |
25
+ | `request.contact` | ✗ | ✗ | ✗ | ✓ | ✗ | Contact data (when input is `"$contact$"`) |
26
+ | `whatsapp.business.phone_number` | ✗ | ✗ | ✓ | ✗ | ✗ | Business phone number (E.164) |
27
+ | `whatsapp.business.phone_number_id` | ✗ | ✗ | ✓ | ✗ | ✗ | WhatsApp phone number ID |
28
+ | `whatsapp.client` | ✗ | ✗ | ✓ | ✗ | ✗ | WhatsApp client instance |
29
+ | **Telegram-Specific** |
30
+ | `telegram.client` | ✗ | ✗ | ✗ | ✓ | ✗ | Telegram client instance |
31
+ | `telegram.chat_type` | ✗ | ✗ | ✗ | ✓ | ✗ | Chat type (private, group, supergroup, channel) |
32
+ | `telegram.callback_query_id` | ✗ | ✗ | ✗ | ✓ (callbacks) | ✗ | Callback query ID for inline keyboard responses |
33
+ | `telegram.original_message_id` | ✗ | ✗ | ✗ | ✓ (callbacks) | ✗ | Original message ID that triggered callback |
34
+ | **HTTP-Specific** |
35
+ | `http.method` | ✗ | ✓ | ✗ | ✗ | ✗ | HTTP method (GET/POST) |
36
+ | `http.path` | ✗ | ✓ | ✗ | ✗ | ✗ | Request path |
37
+ | `http.user_agent` | ✗ | ✓ | ✗ | ✗ | ✗ | User agent header |
38
+ | **Intercom-Specific** |
39
+ | `intercom.client` | ✗ | ✗ | ✗ | ✗ | ✓ | Intercom client instance |
40
+ | `intercom.topic` | ✗ | ✗ | ✗ | ✗ | ✓ | Webhook event type |
41
+
42
+
43
+ ## Accessing Variables in Flows
44
+
45
+ ```ruby
46
+ class MyFlow < FlowChat::Flow
47
+ def start
48
+ # Common variables (all gateways)
49
+ user_id = app.context["request.user_id"]
50
+ user_name = app.context["request.user_name"] # Available from WhatsApp, Intercom, HTTP (optional)
51
+ msisdn = app.context["request.msisdn"] # Available from USSD, WhatsApp, HTTP (optional)
52
+ email = app.context["request.email"] # Available from HTTP (optional)
53
+ platform = app.context["request.platform"]
54
+ input = app.context["request.input"]
55
+
56
+ # Or use convenience methods
57
+ user_id = app.user_id
58
+ platform = app.platform
59
+ input = app.input
60
+
61
+ # Platform-specific variables
62
+ case app.platform
63
+ when :whatsapp
64
+ client = app.context["whatsapp.client"]
65
+
66
+ # Handle special input types
67
+ if input == "$location$"
68
+ location = app.context["request.location"]
69
+ lat = location[:latitude]
70
+ lng = location[:longitude]
71
+ end
72
+
73
+ when :telegram
74
+ client = app.context["telegram.client"]
75
+ chat_type = app.context["telegram.chat_type"]
76
+ username = app.context["request.username"] # @username
77
+
78
+ # Handle special input types
79
+ case input
80
+ when "$location$"
81
+ location = app.context["request.location"]
82
+ lat = location["latitude"]
83
+ lng = location["longitude"]
84
+ when "$media$"
85
+ media = app.context["request.media"]
86
+ file_id = media[:file_id]
87
+ media_type = media[:type] # :photo, :document, :voice
88
+ when "$contact$"
89
+ contact = app.context["request.contact"]
90
+ phone = contact[:phone_number]
91
+ end
92
+
93
+ when :intercom
94
+ topic = app.context["intercom.topic"]
95
+ client = app.context["intercom.client"]
96
+
97
+ # Handle events without messages
98
+ if input.nil?
99
+ # Event without user message (e.g., admin-initiated)
100
+ end
101
+
102
+ when :http
103
+ method = app.context["http.method"]
104
+ user_agent = app.context["http.user_agent"]
105
+
106
+ when :ussd
107
+ msisdn = app.context["request.msisdn"]
108
+ end
109
+ end
110
+ end
111
+ ```
112
+
113
+ ## Notes
114
+
115
+ ⁴ **WhatsApp input**: Text for text messages, `"$location$"` for location, `"$media$"` for media (image, document, audio, video, sticker), `"$contact$"` for shared contacts, or button/list reply IDs.
116
+
117
+ ⁵ **Intercom input**: Text content or `nil` for events without user messages.
118
+
119
+ ⁶ **Telegram input**: Text for text messages, callback data for inline keyboard responses, `"$location$"` for location, `"$media$"` for media (photo, video, audio, document, voice, sticker), `"$contact$"` for shared contacts.
120
+
121
+ ## Media Type Reference
122
+
123
+ Both WhatsApp and Telegram set `request.media` with a `:type` symbol when media is received:
124
+
125
+ | Media Type | WhatsApp | Telegram | Additional Fields |
126
+ |------------|----------|----------|-------------------|
127
+ | `:image` / `:photo` | ✓ `:image` | ✓ `:photo` | id/file_id, mime_type, width, height |
128
+ | `:video` | ✓ | ✓ | id/file_id, mime_type, duration, width, height |
129
+ | `:audio` | ✓ | ✓ | id/file_id, mime_type, duration, title, performer |
130
+ | `:voice` | ✗ | ✓ | file_id, mime_type, duration |
131
+ | `:document` | ✓ | ✓ | id/file_id, mime_type, filename |
132
+ | `:sticker` | ✓ | ✓ | id/file_id, emoji, set_name, is_animated |
133
+
134
+ ### Accessing Media in Flows
135
+
136
+ FlowChat provides constants for special input markers:
137
+
138
+ ```ruby
139
+ FlowChat::Input::LOCATION # "$location$"
140
+ FlowChat::Input::MEDIA # "$media$"
141
+ FlowChat::Input::CONTACT # "$contact$"
142
+ FlowChat::Input::START # "$start$" (session marker)
143
+ ```
144
+
145
+ Example usage:
146
+
147
+ ```ruby
148
+ case app.input
149
+ when FlowChat::Input::MEDIA
150
+ media = app.context["request.media"]
151
+
152
+ case media[:type]
153
+ when :photo, :image
154
+ file_id = media[:file_id] || media[:id]
155
+ when :video
156
+ duration = media[:duration]
157
+ when :document
158
+ filename = media[:file_name] || media[:filename]
159
+ when :sticker
160
+ emoji = media[:emoji]
161
+ end
162
+
163
+ when FlowChat::Input::LOCATION
164
+ location = app.context["request.location"]
165
+ lat, lng = location["latitude"], location["longitude"]
166
+
167
+ when FlowChat::Input::CONTACT
168
+ contact = app.context["request.contact"]
169
+ phone = contact[:phone_number]
170
+ end
171
+ ```