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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +43 -0
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +166 -295
  5. data/Rakefile +12 -1
  6. data/SECURITY.md +1 -1
  7. data/docs/architecture.md +52 -479
  8. data/docs/async-background-processing.md +31 -265
  9. data/docs/configuration.md +106 -613
  10. data/docs/factory-pattern.md +27 -324
  11. data/docs/gateway-context-variables.md +140 -143
  12. data/docs/gateway-development.md +86 -650
  13. data/docs/getting-started.md +40 -379
  14. data/docs/instrumentation.md +88 -279
  15. data/docs/platforms/instagram.md +278 -0
  16. data/docs/platforms/messenger.md +205 -0
  17. data/docs/platforms/telegram.md +47 -951
  18. data/docs/platforms/ussd.md +38 -653
  19. data/docs/platforms/whatsapp.md +73 -1321
  20. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  21. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  22. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  23. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  24. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  25. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  26. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  27. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  28. data/docs/testing.md +26 -297
  29. data/examples/http_controller.rb +12 -10
  30. data/examples/intercom_configuration_example.rb +19 -24
  31. data/examples/intercom_controller.rb +8 -20
  32. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  33. data/examples/simulator_controller.rb +0 -1
  34. data/examples/ussd_controller.rb +86 -158
  35. data/examples/whatsapp_controller.rb +16 -15
  36. data/examples/whatsapp_media_examples.rb +27 -79
  37. data/lib/flow_chat/app.rb +129 -11
  38. data/lib/flow_chat/choice_titles.rb +95 -0
  39. data/lib/flow_chat/config.rb +124 -1
  40. data/lib/flow_chat/delivery_error.rb +9 -0
  41. data/lib/flow_chat/http/configuration_error.rb +9 -0
  42. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  43. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  44. data/lib/flow_chat/input.rb +86 -0
  45. data/lib/flow_chat/instagram/client.rb +32 -0
  46. data/lib/flow_chat/instagram/configuration.rb +147 -0
  47. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  48. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  49. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  50. data/lib/flow_chat/instagram/renderer.rb +23 -0
  51. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  52. data/lib/flow_chat/instrumentation.rb +160 -1
  53. data/lib/flow_chat/intercom/client.rb +34 -28
  54. data/lib/flow_chat/intercom/configuration.rb +2 -49
  55. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  56. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  57. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  58. data/lib/flow_chat/intercom/renderer.rb +57 -5
  59. data/lib/flow_chat/media.rb +121 -0
  60. data/lib/flow_chat/messenger/client.rb +264 -0
  61. data/lib/flow_chat/messenger/configuration.rb +103 -0
  62. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  63. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  64. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  65. data/lib/flow_chat/messenger/renderer.rb +150 -0
  66. data/lib/flow_chat/meta/challenge.rb +24 -0
  67. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  68. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  69. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  70. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  71. data/lib/flow_chat/meta/signature.rb +30 -0
  72. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  73. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  74. data/lib/flow_chat/named_configuration.rb +65 -0
  75. data/lib/flow_chat/prompt.rb +13 -16
  76. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  77. data/lib/flow_chat/security.rb +76 -0
  78. data/lib/flow_chat/session/middleware.rb +11 -2
  79. data/lib/flow_chat/simulator/controller.rb +31 -15
  80. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  81. data/lib/flow_chat/telegram/client.rb +47 -4
  82. data/lib/flow_chat/telegram/configuration.rb +2 -42
  83. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  84. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  85. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  86. data/lib/flow_chat/telegram/renderer.rb +10 -2
  87. data/lib/flow_chat/text_truncator.rb +75 -0
  88. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  89. data/lib/flow_chat/version.rb +1 -1
  90. data/lib/flow_chat/whatsapp/client.rb +14 -7
  91. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  92. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  93. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
  94. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  95. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  96. data/lib/flow_chat.rb +0 -10
  97. data/site/.nojekyll +0 -0
  98. data/site/.og-card.html +89 -0
  99. data/site/favicon.svg +6 -0
  100. data/site/index.html +209 -0
  101. data/site/og.png +0 -0
  102. metadata +51 -3
  103. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
@@ -1,171 +1,168 @@
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
1
+ # Gateway context variables
2
+
3
+ Every gateway parses its platform's webhook into a common set of context values. Flows and middleware read these instead of platform-specific request shapes, which is what lets one flow run everywhere. This document lists what each gateway sets.
4
+
5
+ ## All context variables
6
+
7
+ | Variable | USSD Nalo | HTTP Simple | WhatsApp Cloud API | Messenger Send API | Instagram Send API | Telegram Bot API | Intercom API | Description |
8
+ |----------|-----------|-------------|-------------------|--------------------|--------------------|------------------|--------------|-------------|
9
+ | **Common** |
10
+ | `request.id` | Session id | From user_params | Phone number | PSID | IGSID | Chat id | Conversation id | Session or conversation identifier |
11
+ | `request.user_id` | = msisdn | From user_params | Phone number | PSID | IGSID | Telegram user id | Contact id | Stable per-user identifier |
12
+ | `request.user_name` | none | optional | optional | none | none | First and last name | optional | Sender display name |
13
+ | `request.username` | none | none | none | none | none | Telegram @username | none | Telegram username |
14
+ | `request.msisdn` | set | optional | set | nil | nil | none | optional | E.164 phone number |
15
+ | `request.email` | none | optional | none | none | none | none | optional | User email |
16
+ | `request.message_id` | UUID | UUID | WhatsApp id | `mid` | `mid` | Telegram msg id | optional | Message identifier |
17
+ | `request.timestamp` | Current | Current | Current | Current | Current | From message | Current | ISO8601 timestamp |
18
+ | `request.gateway` | `:nalo` | `:http_simple` | `:whatsapp_cloud_api` | `:messenger_send_api` | `:instagram_send_api` | `:telegram_bot_api` | `:intercom_api` | Gateway symbol |
19
+ | `request.platform` | `:ussd` | `:http` | `:whatsapp` | `:messenger` | `:instagram` | `:telegram` | `:intercom` | Platform symbol |
20
+ | `request.body` | set | set | set | set | set | set | set | Raw request body, string keys |
21
+ | `request.input` | Text | Text | Text (note 1) | Text (note 4) | Text (note 4) | Text (note 2) | Text or nil (note 3) | The turn's text |
22
+ | **Structured attachments** |
23
+ | `request.location` | none | none | set | set | set | set | none | Location payload |
24
+ | `request.media` | none | via `media_url` | set | set | set | set | set (may be several) | Media metadata |
25
+ | `request.contact` | none | none | none | none | none | set | none | Contact payload |
26
+ | **WhatsApp** |
27
+ | `whatsapp.business.phone_number` | | | E.164 business number | | | | | |
28
+ | `whatsapp.business.phone_number_id` | | | WhatsApp phone number id | | | | | |
29
+ | `whatsapp.client` | | | client instance | | | | | |
30
+ | **Messenger** |
31
+ | `messenger.account.id` | | | | Page id | | | | |
32
+ | `messenger.client` | | | | client instance | | | | |
33
+ | **Instagram** |
34
+ | `instagram.account.id` | | | | | Page id or Instagram account id, whichever the webhook named | | | |
35
+ | `instagram.client` | | | | | client instance | | | |
36
+ | **Telegram** |
37
+ | `telegram.client` | | | | | | client instance | | |
38
+ | `telegram.chat_type` | | | | | | private, group, supergroup, channel | | |
39
+ | `telegram.callback_query_id` | | | | | | on callbacks | | |
40
+ | `telegram.original_message_id` | | | | | | on callbacks | | |
41
+ | **HTTP** |
42
+ | `http.method` | | GET or POST | | | | | | |
43
+ | `http.path` | | Request path | | | | | | |
44
+ | `http.user_agent` | | User agent header | | | | | | |
45
+ | **Intercom** |
46
+ | `intercom.client` | | | | | | | client instance | |
47
+ | `intercom.topic` | | | | | | | Webhook event type | |
48
+
49
+ Notes on `request.input`:
50
+
51
+ 1. WhatsApp: the message text, a media caption, or a button/list reply id. `""` for a structured turn (location, media, contact) that carries no text.
52
+ 2. Telegram: the message text, callback data, or a media caption. `""` for a structured turn with no text.
53
+ 3. Intercom: the message text or body, or `""`/`nil` for turns without text.
54
+ 4. Messenger and Instagram: the message text, a media caption, a quick-reply payload, or a postback payload. `""` for a turn with no text.
55
+
56
+ `request.msisdn` is `nil` on both Messenger and Instagram: neither platform exposes a phone number, only a PSID or IGSID scoped to the app and the connected account. Use `request.user_id` (or `app.user_id`) as the durable per-user identifier; sessions key on it by default for both platforms.
57
+
58
+ 5. Messenger and Instagram set `request.location`, not `request.media`, for a shared location: the gateway inspects the attachment's `type` and routes a `location` attachment to `request.location` with latitude, longitude, and name, the same as WhatsApp and Telegram. Every other attachment type still sets `request.media`.
59
+
60
+ `context.input` is always plain text. There are no `"$media$"`/`"$location$"`/`"$contact$"` sentinel values: a structured turn with no text sets `input` to `""` and carries its payload on `request.media`, `request.location`, or `request.contact`. In flows, read `app.input` (a `FlowChat::Input`) or its accessors, described below.
61
+
62
+ ## Reading the turn in a flow
63
+
64
+ Prefer the accessors on `app` over reading context keys directly. Every turn is a `FlowChat::Input` value object with two independent axes: text and an optional attachment. Text and media can arrive together (a captioned photo); location and contact arrive on their own.
44
65
 
45
66
  ```ruby
46
67
  class MyFlow < FlowChat::Flow
47
68
  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
69
+ # Identity and platform, available on every platform.
70
+ user_id = app.user_id
58
71
  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]
72
+ msisdn = app.msisdn
73
+
74
+ # The turn's text. Always a string, "" when the turn carried no text.
75
+ message = app.text
76
+
77
+ # Branch on the attachment kind, not on a magic input value.
78
+ case app.attachment_type
79
+ when :media
80
+ app.media.each do |item| # always a list; iterate so you never drop extra attachments
81
+ item.type # canonical: :image, :video, :audio, :document, :sticker
82
+ item.raw_type # platform-native: :photo or :voice on Telegram
83
+ item.mime_type
84
+ item.filename
85
+ link = item.url # a fetchable URL, or nil
86
+ bytes = item.download # the raw file bytes, or nil
71
87
  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"]
88
+ when :location
89
+ lat = app.location[:latitude]
90
+ lng = app.location[:longitude]
91
+ when :contact
92
+ name = app.contact[:name]
108
93
  end
109
94
  end
110
95
  end
111
96
  ```
112
97
 
113
- ## Notes
98
+ Accessors, all shortcuts to the `app.input` value object:
99
+
100
+ - `app.text`: the turn's text, or the caption sent with an attachment. Always a string, `""` when there is no text.
101
+ - `app.attachment_type`: `:media`, `:location`, `:contact`, or `nil`, the discriminator to branch on.
102
+ - `app.attachment`: the payload of `attachment_type` (the media list, the location hash, the contact hash, or `nil`).
103
+ - `app.media`: always an `Array<FlowChat::Media>` (empty when none), a list even on single-media platforms, so you iterate uniformly.
104
+ - `app.location`: the location hash, or `nil`.
105
+ - `app.contact`: the shared contact card hash, or `nil`.
106
+ - `app.contact_name`: the sender's display name (distinct from a shared contact card).
114
107
 
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.
108
+ ## Media
116
109
 
117
- **Intercom input**: Text content or `nil` for events without user messages.
110
+ WhatsApp, Messenger, Instagram, Telegram, Intercom, and HTTP set `request.media` for inbound media (USSD is text-only and never sets it). WhatsApp, Messenger, Instagram, and Telegram carry a single item; Intercom may carry several, one per attachment. HTTP callers submit inbound media through the `media_url` request param, with optional `media_type` and `mime_type`.
118
111
 
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.
112
+ A `FlowChat::Media` item's `type` is a normalized value; `raw_type` is the platform-native value. The normalization maps `:photo` to `:image` and `:voice` to `:audio`, so `type` is one of `:image`, `:video`, `:audio`, `:document`, `:sticker`.
120
113
 
121
- ## Media Type Reference
114
+ | Media type | WhatsApp | Messenger / Instagram | Telegram | Fields |
115
+ |------------|----------|------------------------|----------|--------|
116
+ | `:image` (Telegram `:photo`) | yes | yes | yes | id or file_id, mime_type, width, height |
117
+ | `:video` | yes | yes | yes | id or file_id, mime_type, duration, width, height |
118
+ | `:audio` | yes | yes | yes | id or file_id, mime_type, duration, title, performer |
119
+ | `:voice` | no | no | yes | file_id, mime_type, duration |
120
+ | `:document` (Messenger/Instagram `file`) | yes | yes | yes | id or file_id, mime_type, filename |
121
+ | `:sticker` | yes | not confirmed | yes | id or file_id, emoji, set_name, is_animated |
122
122
 
123
- Both WhatsApp and Telegram set `request.media` with a `:type` symbol when media is received:
123
+ Messenger and Instagram attachments carry only `type` and `url`; there is no separate mime_type, filename, width, height, or duration, because the gateway does not call a lookup API the way WhatsApp does for a media id. `item.url` resolves a fetchable URL per platform (WhatsApp `get_media_url`, Telegram `getFile`, Messenger/Instagram/Intercom/HTTP use the direct URL already on the attachment). `item.download` returns the raw bytes.
124
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 |
125
+ Meta routes a shared location to `request.location`, not `request.media`, the same as WhatsApp and Telegram - a location share is not a `FlowChat::Media` item on any of these platforms.
133
126
 
134
- ### Accessing Media in Flows
127
+ ### Inspecting attachments in validate and transform
135
128
 
136
- FlowChat provides constants for special input markers:
129
+ The `FlowChat::Input` object behaves like its text for string operations, so validators and transforms read naturally, and it also exposes the attachment:
137
130
 
138
131
  ```ruby
139
- FlowChat::Input::LOCATION # "$location$"
140
- FlowChat::Input::MEDIA # "$media$"
141
- FlowChat::Input::CONTACT # "$contact$"
142
- FlowChat::Input::START # "$start$" (session marker)
132
+ app.screen(:photo) do |prompt|
133
+ prompt.ask "Send your ID photo",
134
+ validate: ->(input) { "Please attach a photo" unless input.media.any? }
135
+ end
136
+
137
+ app.screen(:name) do |prompt|
138
+ prompt.ask "Your name?", transform: ->(input) { input.strip.titleize }
139
+ end
143
140
  ```
144
141
 
145
- Example usage:
142
+ A turn counts as answered (`input.submitted?`) when it has text or an attachment, so a caption-less photo still satisfies a screen.
143
+
144
+ ### What a screen should return
145
+
146
+ `prompt.ask` and `prompt.select` return a string (the text, or your `transform`'s result), and that string is what gets stored as the screen's answer. Return those from screen blocks.
147
+
148
+ `prompt.user_input` is the raw `FlowChat::Input` object, useful inside a `validate` or `transform` when you want the attachment, but avoid returning it as a screen's value:
146
149
 
147
150
  ```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
151
+ # Good: persists a string.
152
+ name = app.screen(:name) { |prompt| prompt.ask "Your name?" }
153
+
154
+ # Avoid: persists the whole Input object into the session store.
155
+ raw = app.screen(:raw) { |prompt| prompt.user_input }
156
+ ```
162
157
 
163
- when FlowChat::Input::LOCATION
164
- location = app.context["request.location"]
165
- lat, lng = location["latitude"], location["longitude"]
158
+ Whatever a screen returns is stored in the session and serialized by the store (`CacheSessionStore` uses `Marshal`). A `FlowChat::Media` serializes without its live platform client, so a media object deserialized from the session has no client and its `url` and `download` return `nil` rather than raising. Fetch media during the turn it arrives, while the client is present; do not rely on downloading it from a stored answer on a later turn.
166
159
 
167
- when FlowChat::Input::CONTACT
168
- contact = app.context["request.contact"]
169
- phone = contact[:phone_number]
170
- end
160
+ ### Lower-level access
161
+
162
+ The raw request hashes remain available if you need them, but prefer the accessors above:
163
+
164
+ ```ruby
165
+ app.context["request.media"] # raw Hash, or Array for Intercom
166
+ app.context["request.location"]
167
+ app.context["request.contact"]
171
168
  ```