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
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
# Inbound Media Support Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **Status: Superseded (historical).** This plan reflects the original approach. The
|
|
4
|
+
> shipped implementation diverged: input sentinels (`FlowChat::Input::MEDIA`/`$media$`,
|
|
5
|
+
> etc.) were replaced by a `FlowChat::Input` turn value object (`context.input` is
|
|
6
|
+
> always plain text), and `app.media` is now **always** an `Array<FlowChat::Media>`
|
|
7
|
+
> (the `media_items` accessor described below was dropped). See
|
|
8
|
+
> `docs/gateway-context-variables.md` for the current API.
|
|
9
|
+
|
|
10
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
11
|
+
|
|
12
|
+
**Goal:** Expose inbound media (and location/contact) to flows across WhatsApp, Telegram, Intercom, and HTTP via a `FlowChat::Media` value object with metadata plus `url`/`download`.
|
|
13
|
+
|
|
14
|
+
**Architecture:** A new `FlowChat::Media` value object encapsulates all cross-platform inbound-media quirks (WhatsApp media-id, Telegram file_id, Intercom/HTTP direct URL) behind `#url`/`#download`. `App` normalizes `context["request.media"]` (single hash or array) into `Array<FlowChat::Media>` via `media_items`, with `media` as the first-item convenience. The Telegram client gains `getFile`-based download; the Intercom and HTTP gateways gain inbound media parsing (Intercom supports multiple attachments per message).
|
|
15
|
+
|
|
16
|
+
**Tech Stack:** Ruby, Zeitwerk autoloading, Minitest (minitest/mock), Net::HTTP.
|
|
17
|
+
|
|
18
|
+
**User Verification:** NO — no user verification required.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: `FlowChat::Media` value object
|
|
23
|
+
|
|
24
|
+
**Goal:** A value object wrapping one inbound media item, exposing metadata plus per-platform `url`/`download`.
|
|
25
|
+
|
|
26
|
+
**Files:**
|
|
27
|
+
- Create: `lib/flow_chat/media.rb`
|
|
28
|
+
- Test: `test/unit/media_test.rb`
|
|
29
|
+
|
|
30
|
+
**Acceptance Criteria:**
|
|
31
|
+
- [ ] Readers expose `type`, `mime_type`, `caption`, `filename` (handles `:filename` and `:file_name`), `id`, `file_id`, `[]`, `to_h`.
|
|
32
|
+
- [ ] `#url` dispatches: WhatsApp → `client.get_media_url(id)`; Telegram → `client.file_url(file_id)`; else → `data[:url]`.
|
|
33
|
+
- [ ] `#download` dispatches: WhatsApp → `client.download_media(id)`; Telegram → `client.download_file(file_id)`; else → HTTP GET of `#url`.
|
|
34
|
+
- [ ] Autoloads as `FlowChat::Media` with no explicit require (Zeitwerk).
|
|
35
|
+
|
|
36
|
+
**Verify:** `ruby -Itest test/unit/media_test.rb -v` → all pass
|
|
37
|
+
|
|
38
|
+
**Steps:**
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 1: Write the failing test**
|
|
41
|
+
|
|
42
|
+
Create `test/unit/media_test.rb`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "test_helper"
|
|
46
|
+
|
|
47
|
+
class MediaTest < Minitest::Test
|
|
48
|
+
def test_metadata_readers
|
|
49
|
+
m = FlowChat::Media.new(
|
|
50
|
+
{type: :image, mime_type: "image/jpeg", caption: "hi", filename: "a.jpg", id: "MID"},
|
|
51
|
+
platform: :whatsapp
|
|
52
|
+
)
|
|
53
|
+
assert_equal :image, m.type
|
|
54
|
+
assert_equal "image/jpeg", m.mime_type
|
|
55
|
+
assert_equal "hi", m.caption
|
|
56
|
+
assert_equal "a.jpg", m.filename
|
|
57
|
+
assert_equal "MID", m.id
|
|
58
|
+
assert_equal "image/jpeg", m[:mime_type]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_filename_falls_back_to_file_name_key
|
|
62
|
+
m = FlowChat::Media.new({type: :document, file_name: "doc.pdf"}, platform: :telegram)
|
|
63
|
+
assert_equal "doc.pdf", m.filename
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_whatsapp_url_and_download_delegate_to_client
|
|
67
|
+
client = Minitest::Mock.new
|
|
68
|
+
client.expect(:get_media_url, "https://cdn/x.jpg", ["MID"])
|
|
69
|
+
client.expect(:download_media, "BYTES", ["MID"])
|
|
70
|
+
m = FlowChat::Media.new({type: :image, id: "MID"}, platform: :whatsapp, client: client)
|
|
71
|
+
assert_equal "https://cdn/x.jpg", m.url
|
|
72
|
+
assert_equal "BYTES", m.download
|
|
73
|
+
client.verify
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_telegram_url_and_download_delegate_to_client
|
|
77
|
+
client = Minitest::Mock.new
|
|
78
|
+
client.expect(:file_url, "https://tg/file.jpg", ["FID"])
|
|
79
|
+
client.expect(:download_file, "TGBYTES", ["FID"])
|
|
80
|
+
m = FlowChat::Media.new({type: :photo, file_id: "FID"}, platform: :telegram, client: client)
|
|
81
|
+
assert_equal "https://tg/file.jpg", m.url
|
|
82
|
+
assert_equal "TGBYTES", m.download
|
|
83
|
+
client.verify
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_url_based_platform_uses_direct_url
|
|
87
|
+
m = FlowChat::Media.new({type: :image, url: "https://intercom/a.png"}, platform: :intercom)
|
|
88
|
+
assert_equal "https://intercom/a.png", m.url
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
94
|
+
|
|
95
|
+
Run: `ruby -Itest test/unit/media_test.rb -v`
|
|
96
|
+
Expected: FAIL — `uninitialized constant FlowChat::Media`
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 3: Write the implementation**
|
|
99
|
+
|
|
100
|
+
Create `lib/flow_chat/media.rb`:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
require "net/http"
|
|
104
|
+
require "uri"
|
|
105
|
+
|
|
106
|
+
module FlowChat
|
|
107
|
+
# Value object wrapping a single inbound media item parsed by a gateway.
|
|
108
|
+
# Normalizes cross-platform differences (WhatsApp media-id, Telegram file_id,
|
|
109
|
+
# Intercom/HTTP direct URL) behind #url and #download.
|
|
110
|
+
class Media
|
|
111
|
+
attr_reader :platform, :client
|
|
112
|
+
|
|
113
|
+
def initialize(data, platform:, client: nil)
|
|
114
|
+
@data = data
|
|
115
|
+
@platform = platform
|
|
116
|
+
@client = client
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def type
|
|
120
|
+
@data[:type]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def mime_type
|
|
124
|
+
@data[:mime_type]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def caption
|
|
128
|
+
@data[:caption]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def filename
|
|
132
|
+
@data[:filename] || @data[:file_name]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def id
|
|
136
|
+
@data[:id]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def file_id
|
|
140
|
+
@data[:file_id]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def [](key)
|
|
144
|
+
@data[key]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def to_h
|
|
148
|
+
@data
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Resolve a fetchable URL for the media.
|
|
152
|
+
def url
|
|
153
|
+
case platform
|
|
154
|
+
when :whatsapp then client.get_media_url(id)
|
|
155
|
+
when :telegram then client.file_url(file_id)
|
|
156
|
+
else @data[:url]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Fetch the raw bytes of the media.
|
|
161
|
+
def download
|
|
162
|
+
case platform
|
|
163
|
+
when :whatsapp then client.download_media(id)
|
|
164
|
+
when :telegram then client.download_file(file_id)
|
|
165
|
+
else fetch(url)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def fetch(resource_url)
|
|
172
|
+
return nil unless resource_url
|
|
173
|
+
|
|
174
|
+
uri = URI(resource_url)
|
|
175
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
176
|
+
http.use_ssl = (uri.scheme == "https")
|
|
177
|
+
response = http.get(uri.request_uri)
|
|
178
|
+
response.body if response.is_a?(Net::HTTPSuccess)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
185
|
+
|
|
186
|
+
Run: `ruby -Itest test/unit/media_test.rb -v`
|
|
187
|
+
Expected: PASS (all 5 tests)
|
|
188
|
+
|
|
189
|
+
- [ ] **Step 5: Commit**
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
git add lib/flow_chat/media.rb test/unit/media_test.rb
|
|
193
|
+
git commit -m "feat: add FlowChat::Media inbound media value object"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```json:metadata
|
|
197
|
+
{"files": ["lib/flow_chat/media.rb", "test/unit/media_test.rb"], "verifyCommand": "ruby -Itest test/unit/media_test.rb -v", "acceptanceCriteria": ["metadata readers", "url/download dispatch per platform", "autoloads via Zeitwerk"], "requiresUserVerification": false}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### Task 2: Telegram client media download
|
|
203
|
+
|
|
204
|
+
**Goal:** Add `get_file`, `file_url`, and `download_file` to the Telegram client so inbound Telegram media can be fetched.
|
|
205
|
+
|
|
206
|
+
**Files:**
|
|
207
|
+
- Modify: `lib/flow_chat/telegram/client.rb` (add public methods before `private` at line 182)
|
|
208
|
+
- Test: `test/unit/telegram/client_test.rb`
|
|
209
|
+
|
|
210
|
+
**Acceptance Criteria:**
|
|
211
|
+
- [ ] `get_file(file_id)` calls the `getFile` API method with `{file_id:}`.
|
|
212
|
+
- [ ] `file_url(file_id)` returns `https://api.telegram.org/file/bot<bot_token>/<file_path>`, or `nil` if no `file_path`.
|
|
213
|
+
- [ ] `download_file(file_id)` GETs `file_url` and returns the body on success, `nil` otherwise.
|
|
214
|
+
|
|
215
|
+
**Verify:** `ruby -Itest test/unit/telegram/client_test.rb -v` → all pass
|
|
216
|
+
|
|
217
|
+
**Steps:**
|
|
218
|
+
|
|
219
|
+
- [ ] **Step 1: Write the failing test**
|
|
220
|
+
|
|
221
|
+
Add to `test/unit/telegram/client_test.rb` (match the file's existing setup — a `FlowChat::Telegram::Configuration` with `bot_token` and a `FlowChat::Telegram::Client`). Add:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
def test_get_file_calls_getFile
|
|
225
|
+
client = FlowChat::Telegram::Client.new(@config)
|
|
226
|
+
client.stub(:api_request, {"ok" => true, "result" => {"file_path" => "photos/f.jpg"}}) do
|
|
227
|
+
result = client.get_file("FID")
|
|
228
|
+
assert_equal "photos/f.jpg", result.dig("result", "file_path")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_file_url_builds_download_url
|
|
233
|
+
client = FlowChat::Telegram::Client.new(@config)
|
|
234
|
+
client.stub(:get_file, {"ok" => true, "result" => {"file_path" => "photos/f.jpg"}}) do
|
|
235
|
+
assert_equal "https://api.telegram.org/file/bot#{@config.bot_token}/photos/f.jpg", client.file_url("FID")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_file_url_returns_nil_without_file_path
|
|
240
|
+
client = FlowChat::Telegram::Client.new(@config)
|
|
241
|
+
client.stub(:get_file, {"ok" => false}) do
|
|
242
|
+
assert_nil client.file_url("FID")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Note: if the existing test file defines `@config`/`@client` differently in `setup`, reuse that instead of re-instantiating.
|
|
248
|
+
|
|
249
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
250
|
+
|
|
251
|
+
Run: `ruby -Itest test/unit/telegram/client_test.rb -v`
|
|
252
|
+
Expected: FAIL — `NoMethodError: undefined method 'get_file'`
|
|
253
|
+
|
|
254
|
+
- [ ] **Step 3: Write the implementation**
|
|
255
|
+
|
|
256
|
+
In `lib/flow_chat/telegram/client.rb`, immediately before the `private` keyword (line 182), add:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# Get file metadata (including file_path) for an inbound file_id
|
|
260
|
+
def get_file(file_id)
|
|
261
|
+
api_request("getFile", {file_id: file_id})
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Build the download URL for an inbound file_id
|
|
265
|
+
def file_url(file_id)
|
|
266
|
+
file_path = get_file(file_id).dig("result", "file_path")
|
|
267
|
+
return nil unless file_path
|
|
268
|
+
|
|
269
|
+
"https://api.telegram.org/file/bot#{@config.bot_token}/#{file_path}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Download the raw bytes for an inbound file_id
|
|
273
|
+
def download_file(file_id)
|
|
274
|
+
url = file_url(file_id)
|
|
275
|
+
return nil unless url
|
|
276
|
+
|
|
277
|
+
uri = URI(url)
|
|
278
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
279
|
+
http.use_ssl = true
|
|
280
|
+
response = http.get(uri.request_uri)
|
|
281
|
+
|
|
282
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
283
|
+
response.body
|
|
284
|
+
else
|
|
285
|
+
FlowChat.logger.error { "Telegram::Client: File download error: #{response.code}" }
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
292
|
+
|
|
293
|
+
Run: `ruby -Itest test/unit/telegram/client_test.rb -v`
|
|
294
|
+
Expected: PASS
|
|
295
|
+
|
|
296
|
+
- [ ] **Step 5: Commit**
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
git add lib/flow_chat/telegram/client.rb test/unit/telegram/client_test.rb
|
|
300
|
+
git commit -m "feat: add Telegram client inbound file download (getFile)"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```json:metadata
|
|
304
|
+
{"files": ["lib/flow_chat/telegram/client.rb", "test/unit/telegram/client_test.rb"], "verifyCommand": "ruby -Itest test/unit/telegram/client_test.rb -v", "acceptanceCriteria": ["get_file", "file_url", "download_file"], "requiresUserVerification": false}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
### Task 3: Wire `App` to expose inbound media, location, and contact
|
|
310
|
+
|
|
311
|
+
**Goal:** Replace the `nil` stubs in `App` so parsed inbound data reaches flows, normalizing single/array media into `media_items` with `media` as the first-item convenience.
|
|
312
|
+
|
|
313
|
+
**Files:**
|
|
314
|
+
- Modify: `lib/flow_chat/app.rb` (methods at lines 66-76; add private helper)
|
|
315
|
+
- Test: `test/integration/media_support_test.rb`
|
|
316
|
+
|
|
317
|
+
**Acceptance Criteria:**
|
|
318
|
+
- [ ] `media_items` returns `[]` when no media, one `FlowChat::Media` for a single hash, N for an array.
|
|
319
|
+
- [ ] `media` returns `media_items.first` (or `nil`).
|
|
320
|
+
- [ ] `location` returns `context["request.location"]`; `contact` returns `context["request.contact"]`; `contact_name` returns `context["request.user_name"]`.
|
|
321
|
+
- [ ] Each `FlowChat::Media` gets the right platform client from context.
|
|
322
|
+
|
|
323
|
+
**Verify:** `ruby -Itest test/integration/media_support_test.rb -v` → all pass
|
|
324
|
+
|
|
325
|
+
**Steps:**
|
|
326
|
+
|
|
327
|
+
- [ ] **Step 1: Write the failing test**
|
|
328
|
+
|
|
329
|
+
Add to `test/integration/media_support_test.rb`:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# ==========================================================================
|
|
333
|
+
# INBOUND MEDIA EXPOSURE
|
|
334
|
+
# ==========================================================================
|
|
335
|
+
|
|
336
|
+
def test_whatsapp_inbound_media_exposed_via_app
|
|
337
|
+
@whatsapp_context.input = FlowChat::Input::MEDIA
|
|
338
|
+
@whatsapp_context["request.platform"] = :whatsapp
|
|
339
|
+
@whatsapp_context["request.media"] = {type: :image, id: "MID", mime_type: "image/jpeg", caption: "hi"}
|
|
340
|
+
app = FlowChat::App.new(@whatsapp_context)
|
|
341
|
+
|
|
342
|
+
assert_instance_of FlowChat::Media, app.media
|
|
343
|
+
assert_equal :image, app.media.type
|
|
344
|
+
assert_equal "MID", app.media.id
|
|
345
|
+
assert_equal 1, app.media_items.size
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_intercom_inbound_multiple_media_exposed_via_app
|
|
349
|
+
ctx = FlowChat::Context.new
|
|
350
|
+
ctx.session = create_test_session_store
|
|
351
|
+
ctx["request.platform"] = :intercom
|
|
352
|
+
ctx["request.media"] = [
|
|
353
|
+
{type: :image, url: "https://i/1.png", mime_type: "image/png"},
|
|
354
|
+
{type: :document, url: "https://i/2.pdf", mime_type: "application/pdf"}
|
|
355
|
+
]
|
|
356
|
+
app = FlowChat::App.new(ctx)
|
|
357
|
+
|
|
358
|
+
assert_equal 2, app.media_items.size
|
|
359
|
+
assert_equal "https://i/1.png", app.media.url
|
|
360
|
+
assert_equal :document, app.media_items.last.type
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def test_inbound_media_absent_returns_nil_and_empty
|
|
364
|
+
@whatsapp_context.input = "just text"
|
|
365
|
+
@whatsapp_context["request.platform"] = :whatsapp
|
|
366
|
+
app = FlowChat::App.new(@whatsapp_context)
|
|
367
|
+
|
|
368
|
+
assert_nil app.media
|
|
369
|
+
assert_equal [], app.media_items
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def test_location_and_contact_exposed_via_app
|
|
373
|
+
@whatsapp_context["request.platform"] = :whatsapp
|
|
374
|
+
@whatsapp_context["request.location"] = {latitude: 1.0, longitude: 2.0}
|
|
375
|
+
@whatsapp_context["request.user_name"] = "John Doe"
|
|
376
|
+
@whatsapp_context["request.contact"] = {name: "Jane", first_name: "Jane"}
|
|
377
|
+
app = FlowChat::App.new(@whatsapp_context)
|
|
378
|
+
|
|
379
|
+
assert_equal 1.0, app.location[:latitude]
|
|
380
|
+
assert_equal "John Doe", app.contact_name
|
|
381
|
+
assert_equal "Jane", app.contact[:name]
|
|
382
|
+
end
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
386
|
+
|
|
387
|
+
Run: `ruby -Itest test/integration/media_support_test.rb -v`
|
|
388
|
+
Expected: FAIL — `app.media` is `nil` / `NoMethodError: undefined method 'media_items'`
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 3: Write the implementation**
|
|
391
|
+
|
|
392
|
+
In `lib/flow_chat/app.rb`, replace the three stub methods (lines 66-76):
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
def contact_name
|
|
396
|
+
context["request.user_name"]
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def contact
|
|
400
|
+
context["request.contact"]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def location
|
|
404
|
+
context["request.location"]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def media
|
|
408
|
+
media_items.first
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def media_items
|
|
412
|
+
raw = context["request.media"]
|
|
413
|
+
return [] unless raw
|
|
414
|
+
|
|
415
|
+
items = raw.is_a?(Array) ? raw : [raw]
|
|
416
|
+
items.map { |data| FlowChat::Media.new(data, platform: platform, client: media_client) }
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Then add to the `protected`/`private` section (after `prepare_user_input`):
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
def media_client
|
|
424
|
+
case platform
|
|
425
|
+
when :whatsapp then context["whatsapp.client"]
|
|
426
|
+
when :telegram then context["telegram.client"]
|
|
427
|
+
when :intercom then context["intercom.client"]
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
433
|
+
|
|
434
|
+
Run: `ruby -Itest test/integration/media_support_test.rb -v`
|
|
435
|
+
Expected: PASS
|
|
436
|
+
|
|
437
|
+
- [ ] **Step 5: Commit**
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
git add lib/flow_chat/app.rb test/integration/media_support_test.rb
|
|
441
|
+
git commit -m "feat: expose inbound media, location, and contact to flows"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```json:metadata
|
|
445
|
+
{"files": ["lib/flow_chat/app.rb", "test/integration/media_support_test.rb"], "verifyCommand": "ruby -Itest test/integration/media_support_test.rb -v", "acceptanceCriteria": ["media_items normalizes single/array/none", "media is first item", "location/contact/contact_name exposed", "correct platform client"], "requiresUserVerification": false}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
### Task 4: Intercom gateway inbound media parsing
|
|
451
|
+
|
|
452
|
+
**Goal:** Parse Intercom message attachments (an array) into `context["request.media"]`, supporting multiple attachments per message.
|
|
453
|
+
|
|
454
|
+
**Files:**
|
|
455
|
+
- Modify: `lib/flow_chat/intercom/gateway/intercom_api.rb` (`extract_latest_user_message`, lines 266-299; and the caller that sets `context.input`, ~line 143-146)
|
|
456
|
+
- Test: `test/unit/intercom/gateway/intercom_api_test.rb`
|
|
457
|
+
|
|
458
|
+
**Acceptance Criteria:**
|
|
459
|
+
- [ ] Attachments on the initial message (`source`) and on reply parts are extracted into an array of media hashes.
|
|
460
|
+
- [ ] `content_type` maps to type: `image/*`→`:image`, `video/*`→`:video`, `audio/*`→`:audio`, else `:document`.
|
|
461
|
+
- [ ] When attachments exist, `context["request.media"]` is set and `context.input = FlowChat::Input::MEDIA`.
|
|
462
|
+
- [ ] Messages without attachments are unaffected (text still flows through unchanged).
|
|
463
|
+
|
|
464
|
+
**Verify:** `ruby -Itest test/unit/intercom/gateway/intercom_api_test.rb -v` → all pass
|
|
465
|
+
|
|
466
|
+
**Steps:**
|
|
467
|
+
|
|
468
|
+
- [ ] **Step 1: Write the failing test**
|
|
469
|
+
|
|
470
|
+
Add to `test/unit/intercom/gateway/intercom_api_test.rb` a test that drives the extraction. Match the file's existing style for building a gateway + webhook body. The core assertion:
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
def test_extract_maps_multiple_attachments_to_media_array
|
|
474
|
+
gateway = build_gateway # use existing helper/setup in this file
|
|
475
|
+
conversation = {
|
|
476
|
+
"source" => {
|
|
477
|
+
"id" => "msg_1",
|
|
478
|
+
"body" => "see attached",
|
|
479
|
+
"attachments" => [
|
|
480
|
+
{"name" => "a.png", "url" => "https://i/a.png", "content_type" => "image/png"},
|
|
481
|
+
{"name" => "b.pdf", "url" => "https://i/b.pdf", "content_type" => "application/pdf"}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
result = gateway.send(:extract_latest_user_message, conversation, "conversation.user.created")
|
|
486
|
+
assert_equal 2, result[:media].size
|
|
487
|
+
assert_equal :image, result[:media][0][:type]
|
|
488
|
+
assert_equal :document, result[:media][1][:type]
|
|
489
|
+
assert_equal "https://i/a.png", result[:media][0][:url]
|
|
490
|
+
end
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Note: `extract_latest_user_message` returns a hash `{id:, body:}`. This task extends it to also include `media:` when attachments are present. If the existing test file lacks a `build_gateway` helper, instantiate the gateway the same way `setup` in that file does.
|
|
494
|
+
|
|
495
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
496
|
+
|
|
497
|
+
Run: `ruby -Itest test/unit/intercom/gateway/intercom_api_test.rb -v`
|
|
498
|
+
Expected: FAIL — `result[:media]` is `nil`
|
|
499
|
+
|
|
500
|
+
- [ ] **Step 3: Write the implementation**
|
|
501
|
+
|
|
502
|
+
In `lib/flow_chat/intercom/gateway/intercom_api.rb`, add a private helper and use it in both branches of `extract_latest_user_message`. Add near the other private helpers:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
def extract_attachments(raw)
|
|
506
|
+
attachments = raw["attachments"] || []
|
|
507
|
+
return nil if attachments.empty?
|
|
508
|
+
|
|
509
|
+
attachments.map do |a|
|
|
510
|
+
{
|
|
511
|
+
type: intercom_media_type(a["content_type"]),
|
|
512
|
+
url: a["url"],
|
|
513
|
+
mime_type: a["content_type"],
|
|
514
|
+
filename: a["name"]
|
|
515
|
+
}
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def intercom_media_type(content_type)
|
|
520
|
+
case content_type
|
|
521
|
+
when %r{\Aimage/} then :image
|
|
522
|
+
when %r{\Avideo/} then :video
|
|
523
|
+
when %r{\Aaudio/} then :audio
|
|
524
|
+
else :document
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Update `extract_latest_user_message` to include media. In the `conversation.user.created` branch:
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
when "conversation.user.created"
|
|
533
|
+
source = conversation["source"]
|
|
534
|
+
if source && (source["body"] || source["attachments"]&.any?)
|
|
535
|
+
{
|
|
536
|
+
id: source["id"],
|
|
537
|
+
body: source["body"],
|
|
538
|
+
media: extract_attachments(source)
|
|
539
|
+
}.compact
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
In the `conversation.user.replied` branch, replace the returned hash:
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
if user_parts.any?
|
|
547
|
+
latest_part = user_parts.last
|
|
548
|
+
{
|
|
549
|
+
id: latest_part["id"],
|
|
550
|
+
body: latest_part["body"],
|
|
551
|
+
media: extract_attachments(latest_part)
|
|
552
|
+
}.compact
|
|
553
|
+
end
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Then, in the caller that sets `context.input` (around lines 143-146, where `latest_message` is used), set media and the sentinel input when present:
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
context["request.message_id"] = latest_message[:id]
|
|
560
|
+
if latest_message[:media]
|
|
561
|
+
context["request.media"] = latest_message[:media]
|
|
562
|
+
context.input = FlowChat::Input::MEDIA
|
|
563
|
+
else
|
|
564
|
+
context.input = @client.parse_message(raw_body)
|
|
565
|
+
end
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Adjust to match the exact existing structure around line 143-150 (preserve the debug log line).
|
|
569
|
+
|
|
570
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
571
|
+
|
|
572
|
+
Run: `ruby -Itest test/unit/intercom/gateway/intercom_api_test.rb -v`
|
|
573
|
+
Expected: PASS
|
|
574
|
+
|
|
575
|
+
- [ ] **Step 5: Run the full Intercom + integration suites for regressions**
|
|
576
|
+
|
|
577
|
+
Run: `ruby -Itest test/unit/intercom/gateway/intercom_api_test.rb test/integration/intercom_integration_test.rb -v`
|
|
578
|
+
Expected: PASS
|
|
579
|
+
|
|
580
|
+
- [ ] **Step 6: Commit**
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
git add lib/flow_chat/intercom/gateway/intercom_api.rb test/unit/intercom/gateway/intercom_api_test.rb
|
|
584
|
+
git commit -m "feat: parse inbound Intercom attachments into request media"
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
```json:metadata
|
|
588
|
+
{"files": ["lib/flow_chat/intercom/gateway/intercom_api.rb", "test/unit/intercom/gateway/intercom_api_test.rb"], "verifyCommand": "ruby -Itest test/unit/intercom/gateway/intercom_api_test.rb -v", "acceptanceCriteria": ["attachments array parsed", "content_type mapping", "media sentinel input set", "text unaffected"], "requiresUserVerification": false}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### Task 5: HTTP gateway inbound media parsing
|
|
594
|
+
|
|
595
|
+
**Goal:** Let the HTTP gateway accept inbound media via request params so the simulator and API callers can submit media.
|
|
596
|
+
|
|
597
|
+
**Files:**
|
|
598
|
+
- Modify: `lib/flow_chat/http/gateway/simple.rb` (after `context.input` is set, ~line 47)
|
|
599
|
+
- Test: `test/unit/http/gateway/simple_test.rb`
|
|
600
|
+
|
|
601
|
+
**Acceptance Criteria:**
|
|
602
|
+
- [ ] When params include `media_url`, `context["request.media"]` is set to `{type:, url:, mime_type:}` (type from `media_type` param, default `:document`).
|
|
603
|
+
- [ ] When no text `input` is present but media is, `context.input = FlowChat::Input::MEDIA`.
|
|
604
|
+
- [ ] When text `input` is present, it still takes precedence (existing behavior preserved).
|
|
605
|
+
- [ ] Requests with neither are unaffected.
|
|
606
|
+
|
|
607
|
+
**Verify:** `ruby -Itest test/unit/http/gateway/simple_test.rb -v` → all pass
|
|
608
|
+
|
|
609
|
+
**Steps:**
|
|
610
|
+
|
|
611
|
+
- [ ] **Step 1: Write the failing test**
|
|
612
|
+
|
|
613
|
+
Add to `test/unit/http/gateway/simple_test.rb`, matching the file's existing controller/params mocking style. Core assertions:
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
def test_inbound_media_url_sets_request_media
|
|
617
|
+
# Build controller whose request.params includes media_url (reuse this file's helper)
|
|
618
|
+
controller = build_controller(params: {"media_url" => "https://x/a.jpg", "media_type" => "image", "mime_type" => "image/jpeg"})
|
|
619
|
+
context = FlowChat::Context.new
|
|
620
|
+
context.controller = controller
|
|
621
|
+
gateway = FlowChat::Http::Gateway::Simple.new(->(_ctx) { [:text, "ok", nil, nil] }, {session_id: "s", user_id: "u"})
|
|
622
|
+
gateway.call(context)
|
|
623
|
+
|
|
624
|
+
assert_equal :image, context["request.media"][:type]
|
|
625
|
+
assert_equal "https://x/a.jpg", context["request.media"][:url]
|
|
626
|
+
assert_equal FlowChat::Input::MEDIA, context.input
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def test_text_input_takes_precedence_over_media
|
|
630
|
+
controller = build_controller(params: {"input" => "hello", "media_url" => "https://x/a.jpg"})
|
|
631
|
+
context = FlowChat::Context.new
|
|
632
|
+
context.controller = controller
|
|
633
|
+
gateway = FlowChat::Http::Gateway::Simple.new(->(_ctx) { [:text, "ok", nil, nil] }, {session_id: "s", user_id: "u"})
|
|
634
|
+
gateway.call(context)
|
|
635
|
+
|
|
636
|
+
assert_equal "hello", context.input
|
|
637
|
+
end
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Note: reuse whatever controller/params builder the existing tests in this file use (e.g. a `mock_controller` helper). Do not invent a new mocking approach — mirror the file.
|
|
641
|
+
|
|
642
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
643
|
+
|
|
644
|
+
Run: `ruby -Itest test/unit/http/gateway/simple_test.rb -v`
|
|
645
|
+
Expected: FAIL — `context["request.media"]` is `nil`
|
|
646
|
+
|
|
647
|
+
- [ ] **Step 3: Write the implementation**
|
|
648
|
+
|
|
649
|
+
In `lib/flow_chat/http/gateway/simple.rb`, after the existing `context.input = params["input"].presence || ""` (line 47), add:
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
# Inbound media (optional): callers may submit a media URL
|
|
653
|
+
if params["media_url"].present?
|
|
654
|
+
context["request.media"] = {
|
|
655
|
+
type: (params["media_type"].presence || "document").to_sym,
|
|
656
|
+
url: params["media_url"],
|
|
657
|
+
mime_type: params["mime_type"].presence
|
|
658
|
+
}
|
|
659
|
+
context.input = FlowChat::Input::MEDIA if context.input.blank?
|
|
660
|
+
end
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
664
|
+
|
|
665
|
+
Run: `ruby -Itest test/unit/http/gateway/simple_test.rb -v`
|
|
666
|
+
Expected: PASS
|
|
667
|
+
|
|
668
|
+
- [ ] **Step 5: Commit**
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
git add lib/flow_chat/http/gateway/simple.rb test/unit/http/gateway/simple_test.rb
|
|
672
|
+
git commit -m "feat: parse inbound media params in HTTP gateway"
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
```json:metadata
|
|
676
|
+
{"files": ["lib/flow_chat/http/gateway/simple.rb", "test/unit/http/gateway/simple_test.rb"], "verifyCommand": "ruby -Itest test/unit/http/gateway/simple_test.rb -v", "acceptanceCriteria": ["media_url sets request.media", "media sentinel when no text", "text precedence preserved"], "requiresUserVerification": false}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
### Task 6: Full suite regression + docs
|
|
682
|
+
|
|
683
|
+
**Goal:** Confirm the whole test suite is green and note the new inbound media API in project docs.
|
|
684
|
+
|
|
685
|
+
**Files:**
|
|
686
|
+
- Modify: `README.md` or `docs/` (whichever documents flow APIs — add `app.media` / `app.media_items` / `app.location` / `app.contact` usage)
|
|
687
|
+
|
|
688
|
+
**Acceptance Criteria:**
|
|
689
|
+
- [ ] `rake test` passes with zero failures.
|
|
690
|
+
- [ ] Docs mention inbound media access (`app.media.download`, `media_items` for multi-attachment Intercom).
|
|
691
|
+
|
|
692
|
+
**Verify:** `bundle exec rake test` → 0 failures, 0 errors
|
|
693
|
+
|
|
694
|
+
**Steps:**
|
|
695
|
+
|
|
696
|
+
- [ ] **Step 1: Run the full suite**
|
|
697
|
+
|
|
698
|
+
Run: `bundle exec rake test`
|
|
699
|
+
Expected: 0 failures, 0 errors. Fix any regressions before proceeding.
|
|
700
|
+
|
|
701
|
+
- [ ] **Step 2: Add a short docs section**
|
|
702
|
+
|
|
703
|
+
Find where flow input APIs are documented (grep the README/docs for `app.msisdn` or `app.location`). Add a concise inbound-media example:
|
|
704
|
+
|
|
705
|
+
```ruby
|
|
706
|
+
photo = app.screen(:upload) { |p| p.ask "Send a photo" }
|
|
707
|
+
if app.media
|
|
708
|
+
bytes = app.media.download # raw file bytes
|
|
709
|
+
url = app.media.url # fetchable URL
|
|
710
|
+
app.media.type # :image, :document, ...
|
|
711
|
+
end
|
|
712
|
+
app.media_items # Array (Intercom may attach several)
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
- [ ] **Step 3: Commit**
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
git add -A
|
|
719
|
+
git commit -m "docs: document inbound media access API"
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
```json:metadata
|
|
723
|
+
{"files": ["README.md"], "verifyCommand": "bundle exec rake test", "acceptanceCriteria": ["full suite green", "docs updated"], "requiresUserVerification": false}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## Self-Review Notes
|
|
729
|
+
|
|
730
|
+
- **Spec coverage:** Media object (Task 1), App wiring incl. media_items/location/contact (Task 3), Telegram download (Task 2), Intercom multi-attachment parsing (Task 4), HTTP parsing (Task 5), tests throughout, docs (Task 6). All spec sections mapped.
|
|
731
|
+
- **Type consistency:** `FlowChat::Media.new(data, platform:, client:)`, `media_items`, `media`, `client.file_url`/`download_file`/`get_media_url`/`download_media`, `intercom_media_type` used consistently across tasks.
|
|
732
|
+
- **Verification requirement scan:** Prompt "we need to add support for media" requires no user sign-off → NO. No verification task required.
|