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,391 @@
|
|
|
1
|
+
# Facebook Messenger and Instagram DM Support: Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-10
|
|
4
|
+
**Status:** Implemented on `feat/messenger-instagram`. Two open items below are unverified and flagged in place.
|
|
5
|
+
**Branch context:** builds on `feat/coexistence-webhooks`
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
FlowChat ships gateways for USSD, WhatsApp, Telegram, HTTP and Intercom. Two Meta
|
|
10
|
+
messaging surfaces are missing: Facebook Messenger (Page DMs) and Instagram DMs.
|
|
11
|
+
Both run on the Messenger Platform, which shares an envelope and a Send API with
|
|
12
|
+
each other and shares webhook verification and signature validation with the
|
|
13
|
+
WhatsApp Cloud API gateway already in the tree.
|
|
14
|
+
|
|
15
|
+
Adding them by copying `lib/flow_chat/whatsapp/gateway/cloud_api.rb` would put a
|
|
16
|
+
third and fourth copy of the hub-verification and `X-Hub-Signature-256` logic in the
|
|
17
|
+
gem. That logic changed twice recently (`76b141b`, `f4fce7a`), so the copies would
|
|
18
|
+
drift.
|
|
19
|
+
|
|
20
|
+
## Goals
|
|
21
|
+
|
|
22
|
+
1. Facebook Messenger and Instagram DMs as first-class platforms, at parity with the
|
|
23
|
+
WhatsApp gateway: gateway, configuration, client, renderer, choice mapping,
|
|
24
|
+
inbound and outbound media, async, instrumentation, simulator, docs and tests.
|
|
25
|
+
2. One implementation of the Meta webhook plumbing, shared by all three Meta
|
|
26
|
+
platforms.
|
|
27
|
+
3. Two correctness fixes to the WhatsApp gateway found while designing this.
|
|
28
|
+
|
|
29
|
+
## Non-Goals
|
|
30
|
+
|
|
31
|
+
- The handover protocol beyond publishing `standby` and `messaging_handovers`.
|
|
32
|
+
- Message tags for sends outside the 24 hour window.
|
|
33
|
+
- Persistent menus and ice breakers.
|
|
34
|
+
- The Instagram API with Instagram Login path (`graph.instagram.com`). Only the
|
|
35
|
+
Facebook Login path is supported.
|
|
36
|
+
- Facebook Page feed or comment events. This is DMs only.
|
|
37
|
+
|
|
38
|
+
## Verified platform facts
|
|
39
|
+
|
|
40
|
+
Checked against Meta's documentation on 2026-08-10, because the rendering design
|
|
41
|
+
depends on the exact numbers.
|
|
42
|
+
|
|
43
|
+
| Surface | Messenger | Instagram |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Quick replies | 13 max, title 20 chars, payload 1000 chars | 13 max, title 20 chars, mobile only, plain text only |
|
|
46
|
+
| Generic template (carousel) | 10 elements, 3 buttons per element, `postback` and `web_url` only, title 80, subtitle 80 | same, mobile app only, not on web |
|
|
47
|
+
| Text length | 2000 chars (NOT VERIFIED, the reference page did not render; confirm during implementation) | under 1000 chars, and 1000 bytes UTF-8 |
|
|
48
|
+
| Send endpoint | `POST /{PAGE_ID}/messages`, also `/me/messages` | same, and no `messaging_type` parameter |
|
|
49
|
+
| Rich text | none | none |
|
|
50
|
+
|
|
51
|
+
Webhook envelope for both: `object` at the top level, then `entry[].messaging[]`,
|
|
52
|
+
where each event carries `sender`, `recipient`, `timestamp` and one event-specific
|
|
53
|
+
key. Messenger delivers under `object: "page"`. Whether Instagram via Facebook Login
|
|
54
|
+
delivers under `page` or `instagram` was ambiguous in the docs, so the expected value
|
|
55
|
+
is a subclass hook to be confirmed against the app dashboard, not a constant baked
|
|
56
|
+
into shared code.
|
|
57
|
+
|
|
58
|
+
Messenger webhook fields: `messages`, `message_deliveries`, `message_echoes`,
|
|
59
|
+
`message_edits`, `message_reactions`, `message_reads`, `messaging_account_linking`,
|
|
60
|
+
`messaging_feedback`, `messaging_game_plays`, `messaging_handovers`,
|
|
61
|
+
`messaging_optins`, `messaging_policy_enforcement`, `messaging_postbacks`,
|
|
62
|
+
`messaging_referrals`, `messaging_seen`, `messenger_template_status_update`,
|
|
63
|
+
`response_feedback`, `send_cart`, `standby`.
|
|
64
|
+
|
|
65
|
+
WhatsApp interactive lists, for the fix below: "up to 10 sections, with up to 10 rows
|
|
66
|
+
for all sections combined", row title 24, row description 72, button text 20.
|
|
67
|
+
|
|
68
|
+
## Approach
|
|
69
|
+
|
|
70
|
+
A shared base with two thin gateways.
|
|
71
|
+
|
|
72
|
+
`FlowChat::Meta::` holds what all three Meta platforms share.
|
|
73
|
+
`FlowChat::Meta::MessagingGateway` implements the `entry[].messaging[]` envelope once.
|
|
74
|
+
`Messenger::Gateway::SendApi` and `Instagram::Gateway::SendApi` each subclass it and
|
|
75
|
+
override four hooks: platform symbol, configuration class, renderer class, and the
|
|
76
|
+
account-id check that corresponds to the `phone_number_id` comparison at
|
|
77
|
+
`cloud_api.rb:224`.
|
|
78
|
+
|
|
79
|
+
Instagram subclasses the shared base rather than the Messenger gateway. The two are
|
|
80
|
+
siblings, not parent and child, and making `Instagram` depend on the `Messenger`
|
|
81
|
+
namespace would misstate the relationship.
|
|
82
|
+
|
|
83
|
+
Rejected alternatives:
|
|
84
|
+
|
|
85
|
+
- **One gateway serving both platforms, branching on the webhook `object`.** Fewest
|
|
86
|
+
files, and it mirrors how Meta frames the Messenger Platform, but the class would
|
|
87
|
+
branch on platform for tokens, character limits, rendering capability and event
|
|
88
|
+
types, and the configuration's `valid?` would depend on which half was configured.
|
|
89
|
+
- **Copy the WhatsApp gateway with a `platform:` constructor argument.** Cheapest to
|
|
90
|
+
write and touches nothing existing, at the cost of a third and fourth copy of
|
|
91
|
+
recently changed signature logic.
|
|
92
|
+
|
|
93
|
+
## Components
|
|
94
|
+
|
|
95
|
+
### 1. `FlowChat::Meta` (new)
|
|
96
|
+
|
|
97
|
+
- `lib/flow_chat/meta/webhook_verification.rb`: the `hub.mode` / `hub.verify_token` /
|
|
98
|
+
`hub.challenge` exchange, including the empty-token guard from `cloud_api.rb:89`
|
|
99
|
+
(a configuration with no verify token must verify nothing).
|
|
100
|
+
- `lib/flow_chat/meta/signature_validation.rb`: `X-Hub-Signature-256` HMAC over the
|
|
101
|
+
raw body, using `FlowChat::Security.secure_compare`, with the
|
|
102
|
+
`skip_signature_validation` escape and the "app_secret required" error.
|
|
103
|
+
|
|
104
|
+
`Whatsapp::Gateway::CloudApi` loses its private `handle_verification` and
|
|
105
|
+
`valid_webhook_signature?` and includes these instead. Behavior is unchanged, so
|
|
106
|
+
`test/unit/whatsapp/gateway/cloud_api_test.rb` is the regression net.
|
|
107
|
+
|
|
108
|
+
### 2. `FlowChat::NamedConfiguration` (new)
|
|
109
|
+
|
|
110
|
+
The named-configuration registry (`register`, `get`, `exists?`, `configuration_names`,
|
|
111
|
+
`clear_all!`, `register_as`) is currently duplicated verbatim in
|
|
112
|
+
`whatsapp/configuration.rb:46`, `telegram/configuration.rb:46` and
|
|
113
|
+
`intercom/configuration.rb:53`. The two new platforms would make five copies.
|
|
114
|
+
|
|
115
|
+
Extracted to one module, with **per-class storage**. The existing copies use a
|
|
116
|
+
`@@configurations` class variable; a shared module using `@@` would give every
|
|
117
|
+
platform one merged registry, which is a bug. The module keys storage per including
|
|
118
|
+
class instead.
|
|
119
|
+
|
|
120
|
+
All five classes migrate onto it. Telegram and Intercom have configuration test
|
|
121
|
+
suites. WhatsApp's configuration has no direct test, so its migration leans on the
|
|
122
|
+
gateway tests.
|
|
123
|
+
|
|
124
|
+
### 3. `Messenger::Configuration` and `Instagram::Configuration` (new)
|
|
125
|
+
|
|
126
|
+
`Messenger::Configuration` carries `page_id`, `access_token`, `verify_token`,
|
|
127
|
+
`app_id`, `app_secret`, `skip_signature_validation`. `Instagram::Configuration`
|
|
128
|
+
carries the same attributes, including `app_id` and `app_secret`, and adds
|
|
129
|
+
`instagram_account_id` alongside the linked `page_id`.
|
|
130
|
+
|
|
131
|
+
Both follow `whatsapp/configuration.rb:29`: a `from_credentials` reading `messenger:`
|
|
132
|
+
or `instagram:` from Rails credentials, falling back to `MESSENGER_*` or
|
|
133
|
+
`INSTAGRAM_*` environment variables. `valid?` requires access token, account id and
|
|
134
|
+
verify token.
|
|
135
|
+
|
|
136
|
+
`app_id` is load-bearing here, not decorative: it is what classifies echoes below.
|
|
137
|
+
|
|
138
|
+
`FlowChat::Config.messenger` and `FlowChat::Config.instagram` join
|
|
139
|
+
`FlowChat::Config.whatsapp`, each carrying `api_base_url` plus that platform's text
|
|
140
|
+
and choice limits as named constants rather than literals in the renderer.
|
|
141
|
+
|
|
142
|
+
### 4. Inbound: `Messenger::Gateway::SendApi` (new)
|
|
143
|
+
|
|
144
|
+
Walks `entry[].messaging[]` and dispatches on which key each event carries, following
|
|
145
|
+
the rule established by `27fd234`: model messaging, publish everything else.
|
|
146
|
+
|
|
147
|
+
| Event key | Treatment |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `message` with `is_echo` | Published with a derived origin. Never drives a flow. |
|
|
150
|
+
| `message` | Drives the flow. Input from `text`, or `quick_reply.payload`, or the attachment caption. |
|
|
151
|
+
| `postback` | Drives the flow. Input from `payload`. Covers carousel buttons, Get Started and ice breakers. |
|
|
152
|
+
| `delivery`, `read` | `MESSAGE_STATUS`, mirroring `handle_statuses` at `cloud_api.rb:276`. |
|
|
153
|
+
| anything else | `WEBHOOK_RECEIVED` with the field name and the whole value. |
|
|
154
|
+
|
|
155
|
+
Two behaviors carry over from the WhatsApp gateway deliberately:
|
|
156
|
+
|
|
157
|
+
- The `flow_ran` single-flow guard (`cloud_api.rb:150`). Only one event per delivery
|
|
158
|
+
can drive a flow, because only one can own the HTTP response.
|
|
159
|
+
- The ordering fix from `f17de9c`. Deliveries and reads are handled **before** the
|
|
160
|
+
flow slot is claimed, so a receipt arriving ahead of a message in the same batch
|
|
161
|
+
cannot spend the slot and drop the message.
|
|
162
|
+
|
|
163
|
+
Context values set:
|
|
164
|
+
|
|
165
|
+
| Key | Value |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `request.id`, `request.user_id` | `sender.id`, the PSID or IGSID |
|
|
168
|
+
| `request.msisdn` | `nil`. Neither platform exposes a phone number. |
|
|
169
|
+
| `request.message_id` | `mid` |
|
|
170
|
+
| `request.platform` | `:messenger` or `:instagram` |
|
|
171
|
+
| `request.gateway` | `:messenger_send_api` or `:instagram_send_api` |
|
|
172
|
+
| `request.timestamp`, `request.body` | as WhatsApp sets them |
|
|
173
|
+
| `messenger.page.id` / `instagram.account.id` | the receiving account |
|
|
174
|
+
| `messenger.client` / `instagram.client` | the platform client, for out-of-band sends |
|
|
175
|
+
| `request.media` | attachments, normalized. `file` maps to `:document` to match the existing media contract. |
|
|
176
|
+
|
|
177
|
+
Inbound attachments are simpler than WhatsApp's. Meta puts a direct CDN URL in
|
|
178
|
+
`attachments[].payload.url`, so there is no media-id lookup step. Those URLs are
|
|
179
|
+
signed and expire, which the existing `Media` caveat about eager `download` already
|
|
180
|
+
covers.
|
|
181
|
+
|
|
182
|
+
### 5. Echoes and coexistence
|
|
183
|
+
|
|
184
|
+
An echo is not noise. `message_echoes` reports every message sent on the thread,
|
|
185
|
+
including replies typed by a human in Page Inbox and messages sent by another app.
|
|
186
|
+
An app needs that fact, typically to stop the bot while a human is handling the
|
|
187
|
+
conversation.
|
|
188
|
+
|
|
189
|
+
`app_id` distinguishes the cases, so the gateway derives `echo_origin` before
|
|
190
|
+
publishing:
|
|
191
|
+
|
|
192
|
+
| Condition | `echo_origin` |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `app_id` equals the configured `app_id` | `:self` |
|
|
195
|
+
| `app_id` present and different | `:other_app` |
|
|
196
|
+
| `app_id` absent | `:human_agent` |
|
|
197
|
+
|
|
198
|
+
Published through the existing `WEBHOOK_RECEIVED` event with `field: "message_echoes"`
|
|
199
|
+
and the whole raw value, plus the derived `echo_origin`. This keeps one event
|
|
200
|
+
vocabulary and matches how WhatsApp publishes coexistence echoes, while sparing the
|
|
201
|
+
application from having to know Meta's `app_id` semantics.
|
|
202
|
+
|
|
203
|
+
Echoes never drive a flow. An echo of our own send that drove a flow would loop.
|
|
204
|
+
|
|
205
|
+
### 6. Outbound: clients
|
|
206
|
+
|
|
207
|
+
`Messenger::Client` and `Instagram::Client` expose
|
|
208
|
+
`send_message(recipient_id, text, choices:, media:)` and `upload_media`, wrapped in
|
|
209
|
+
the existing `report_delivery_failure` helper so `on_delivery_failure` and
|
|
210
|
+
`on_delivery_success` fire as they do at `cloud_api.rb:445`.
|
|
211
|
+
|
|
212
|
+
`platform_message_id_from` reads `result["message_id"]`. The Send API returns
|
|
213
|
+
`{recipient_id, message_id}`, flatter than WhatsApp's `messages[0].id`.
|
|
214
|
+
|
|
215
|
+
Sends outside the 24 hour messaging window are not modelled. The send is attempted,
|
|
216
|
+
and Meta's rejection travels the existing delivery-failure path like any other send
|
|
217
|
+
error. No window tracking and no automatic tagging: the gateway would be holding
|
|
218
|
+
state and guessing at policy the application owns.
|
|
219
|
+
|
|
220
|
+
### 7. Outbound: renderers and the choice ladder
|
|
221
|
+
|
|
222
|
+
Neither platform supports rich text, so a third text conversion is needed.
|
|
223
|
+
`Renderers::MarkdownSupport` currently offers `to_html` (used by Telegram) and
|
|
224
|
+
WhatsApp implements its own `to_whatsapp`. A shared `to_plain_text` is added to
|
|
225
|
+
`MarkdownSupport`: strip emphasis, render `ul` as bullets and `ol` as numbers, render
|
|
226
|
+
links as `text (url)`, decode entities.
|
|
227
|
+
|
|
228
|
+
The ladder:
|
|
229
|
+
|
|
230
|
+
| Choices | Messenger | Instagram |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| 0 | text, split at 2000 chars | text, split at 1000 bytes |
|
|
233
|
+
| 1 to 13 | quick replies, title 20, payload is the generated id | quick replies **and** a numbered list in the body |
|
|
234
|
+
| 14 to 30 | carousel, 10 elements by 3 postback buttons, element title 80, button title 20 | carousel **and** a numbered list in the body |
|
|
235
|
+
| above 30 | numbered list in the body, typed number accepted | same |
|
|
236
|
+
|
|
237
|
+
Instagram always carries the numbered list because both of its interactive surfaces
|
|
238
|
+
are mobile only. Without it, a user on desktop Instagram receives a prompt with no
|
|
239
|
+
selectable options and no way to answer, which is a dead-end turn. With it, mobile
|
|
240
|
+
users tap and desktop users type.
|
|
241
|
+
|
|
242
|
+
`Whatsapp::IdGenerator` is extracted to `FlowChat::IdGenerator` with a configurable
|
|
243
|
+
maximum length, since the quick-reply payload cap is 1000 rather than WhatsApp's 256.
|
|
244
|
+
|
|
245
|
+
### 8. Choice mapping
|
|
246
|
+
|
|
247
|
+
A `Middleware::ChoiceMapper` per platform, mapping payloads back to original choice
|
|
248
|
+
keys exactly as `whatsapp/middleware/choice_mapper.rb` does. Each gateway's
|
|
249
|
+
`configure_middleware_stack` inserts it, as `cloud_api.rb:56` does.
|
|
250
|
+
|
|
251
|
+
The mapper stores one mapping per turn whose keys depend on the rung the renderer
|
|
252
|
+
used. On the quick-reply and carousel rungs the keys are generated ids, as WhatsApp
|
|
253
|
+
does today. On the numbered rung the keys are the position strings `"1"`, `"2"` and
|
|
254
|
+
so on, following `Ussd::Middleware::ChoiceMapper`. Both resolve to the original choice
|
|
255
|
+
key, so the flow never sees the difference.
|
|
256
|
+
|
|
257
|
+
On Instagram, where the body carries a numbered list *and* quick replies or a
|
|
258
|
+
carousel, both key sets apply to the same turn. They are stored as **two separate
|
|
259
|
+
maps**, not merged into one, and resolved generated-id first with the position map as
|
|
260
|
+
the fallback.
|
|
261
|
+
|
|
262
|
+
Merging them would be a bug. `IdGenerator#normalize_label` keeps `\w`, which includes
|
|
263
|
+
digits, so a choice labelled `"1"` generates the id `"1"`. Given
|
|
264
|
+
`{"a" => "2", "b" => "1"}`, position `"1"` means choice `a` while generated id `"1"`
|
|
265
|
+
means choice `b`, and a single hash would silently lose one. Two maps with a defined
|
|
266
|
+
precedence make a tap and a typed number resolve independently and correctly.
|
|
267
|
+
|
|
268
|
+
### 9. Sessions
|
|
269
|
+
|
|
270
|
+
`platform_default_identifier` (`session/middleware.rb:93`) currently returns
|
|
271
|
+
`:msisdn` for `:whatsapp` and `:request_id` for everything else. Both new platforms
|
|
272
|
+
are added, returning `:user_id`, so sessions key on the PSID or IGSID explicitly
|
|
273
|
+
rather than relying on `request.id` happening to hold it.
|
|
274
|
+
|
|
275
|
+
### 10. Async, instrumentation, simulator
|
|
276
|
+
|
|
277
|
+
`GatewayAsyncSupport` is included in the base Messenger gateway, so Instagram
|
|
278
|
+
inherits it, and `async_supported?` stays true. The Factory pattern needs no changes,
|
|
279
|
+
so `GenericAsyncJob` covers both platforms unmodified.
|
|
280
|
+
|
|
281
|
+
No new instrumentation event constants. `MESSAGE_RECEIVED`, `MESSAGE_SENT`,
|
|
282
|
+
`MESSAGE_DELIVERY_FAILED`, `MESSAGE_STATUS`, `WEBHOOK_VERIFIED`, `WEBHOOK_FAILED` and
|
|
283
|
+
`WEBHOOK_RECEIVED` already carry `platform` and `gateway` in their payloads.
|
|
284
|
+
|
|
285
|
+
Both platforms are wired into the simulator, reusing the `simulator_mode` flag and
|
|
286
|
+
signed-cookie gating that `simulate?` (`cloud_api.rb:508`) implements.
|
|
287
|
+
|
|
288
|
+
`Messenger::ConfigurationError` and `Instagram::ConfigurationError` mirror
|
|
289
|
+
`Whatsapp::ConfigurationError`.
|
|
290
|
+
|
|
291
|
+
## WhatsApp fixes
|
|
292
|
+
|
|
293
|
+
Both found while designing the choice ladder, both in scope for this work.
|
|
294
|
+
|
|
295
|
+
### Fix 1: interactive lists above 10 choices are rejected
|
|
296
|
+
|
|
297
|
+
`Whatsapp::Renderer#build_list_message` (`renderer.rb:186`) slices more than 10
|
|
298
|
+
choices into multiple sections titled `1-10`, `11-20` and so on. Meta caps a list at
|
|
299
|
+
10 rows for all sections combined, so a 25-choice payload is rejected. The section
|
|
300
|
+
titles read like pagination, but nothing is paged: there is no second message and no
|
|
301
|
+
stored offset.
|
|
302
|
+
|
|
303
|
+
Corrected ladder, consistent with the new platforms:
|
|
304
|
+
|
|
305
|
+
| Choices | Rendering |
|
|
306
|
+
|---|---|
|
|
307
|
+
| 3 or fewer | interactive buttons, title truncated to 20 |
|
|
308
|
+
| 4 to 10 | interactive list, single section, title 24 and description 72 |
|
|
309
|
+
| above 10 | numbered list in the body, typed number accepted |
|
|
310
|
+
|
|
311
|
+
Row title and description truncation in the current code is already correct at 24 and
|
|
312
|
+
72, so only count handling changes. `test/unit/whatsapp/renderer_test.rb:209` asserts
|
|
313
|
+
the three-section payload and is rewritten to assert the fallback.
|
|
314
|
+
|
|
315
|
+
Paged list messages were considered and rejected for now. They work, but they need a
|
|
316
|
+
"More" row, a stored offset and a turn per page, and the numbered fallback gives all
|
|
317
|
+
four platforms one mechanism with no new state.
|
|
318
|
+
|
|
319
|
+
### Fix 2: WhatsApp echoes gain a derived origin
|
|
320
|
+
|
|
321
|
+
`handle_unmodelled_field` (`cloud_api.rb:309`) publishes coexistence echoes with the
|
|
322
|
+
raw value. It derives `echo_origin` the same way as above, so both platforms report a
|
|
323
|
+
human takeover identically.
|
|
324
|
+
|
|
325
|
+
## Testing
|
|
326
|
+
|
|
327
|
+
`test/unit/messenger/` and `test/unit/instagram/` mirroring `test/unit/whatsapp/`:
|
|
328
|
+
gateway, client, renderer, configuration, choice mapper. Plus
|
|
329
|
+
`test/integration/messenger_integration_test.rb` and the Instagram equivalent,
|
|
330
|
+
modelled on `whatsapp_integration_test.rb`.
|
|
331
|
+
|
|
332
|
+
Webhook fixtures covering: text, quick-reply reply, postback, attachment, self echo,
|
|
333
|
+
human-agent echo, delivery, read, and an unmodelled field.
|
|
334
|
+
|
|
335
|
+
Regression coverage for the shared extractions: existing WhatsApp gateway tests for
|
|
336
|
+
`Meta::` verification and signature validation, existing Telegram and Intercom
|
|
337
|
+
configuration tests for `NamedConfiguration`, `test/unit/whatsapp/id_generator_test.rb`
|
|
338
|
+
moved and extended for the configurable maximum length, and the rewritten WhatsApp
|
|
339
|
+
renderer test for the list fix.
|
|
340
|
+
|
|
341
|
+
## Implementation sequence
|
|
342
|
+
|
|
343
|
+
The work is large enough to want an order. Each phase leaves the tree green and is
|
|
344
|
+
reviewable on its own.
|
|
345
|
+
|
|
346
|
+
1. **Shared extractions and WhatsApp fixes.** `Meta::WebhookVerification`,
|
|
347
|
+
`Meta::SignatureValidation`, `NamedConfiguration`, `FlowChat::IdGenerator`,
|
|
348
|
+
`to_plain_text`, and both WhatsApp fixes. Touches only existing code and existing
|
|
349
|
+
tests, and is independently valuable.
|
|
350
|
+
2. **Messenger.** `Meta::MessagingGateway` plus the Messenger configuration, client,
|
|
351
|
+
renderer, choice mapper, gateway, session identifier, simulator, docs and tests.
|
|
352
|
+
3. **Instagram.** The subclass and its configuration, client and renderer, which is
|
|
353
|
+
mostly limits and the always-numbered body, plus docs and tests.
|
|
354
|
+
|
|
355
|
+
Phase 1 landing first means phases 2 and 3 build on shared code that is already
|
|
356
|
+
proven by the existing suites rather than introducing it and its first consumer at
|
|
357
|
+
once.
|
|
358
|
+
|
|
359
|
+
## Documentation
|
|
360
|
+
|
|
361
|
+
New `docs/platforms/messenger.md` and `docs/platforms/instagram.md`. Updates to the
|
|
362
|
+
README platform table (`README.md:74`), the platform-differences table
|
|
363
|
+
(`README.md:186`), the intro sentence listing platforms (`README.md:26`) and the docs
|
|
364
|
+
index (`README.md:236`). `docs/gateway-context-variables.md` gains both gateways.
|
|
365
|
+
|
|
366
|
+
Docs follow the established register: dense plain prose, real limits and edge cases,
|
|
367
|
+
no marketing adjectives, no em-dashes.
|
|
368
|
+
|
|
369
|
+
## Open items for implementation
|
|
370
|
+
|
|
371
|
+
1. **Resolved, negatively.** Meta states no text limit for Messenger on any current
|
|
372
|
+
reference page, unlike Instagram's documented 1,000 bytes. 2000 stays as the
|
|
373
|
+
long-cited figure, with a comment recording that it is unverified. The client
|
|
374
|
+
splits at this value rather than truncating, so being wrong low costs an extra
|
|
375
|
+
message and being wrong high gets a send rejected.
|
|
376
|
+
2. **STILL OPEN, must be checked before Instagram goes live.** Whether Instagram via
|
|
377
|
+
Facebook Login delivers webhooks under `object: "page"` or `object: "instagram"`.
|
|
378
|
+
The gateway currently expects `"instagram"`. A wrong value drops every delivery and
|
|
379
|
+
answers 200, so the symptom is a bot that receives nothing while the dashboard
|
|
380
|
+
reports success. `Meta::MessagingGateway` logs a warning naming the mismatch, so a
|
|
381
|
+
single production delivery reveals the answer. One-line fix in
|
|
382
|
+
`Instagram::Gateway::SendApi#expected_webhook_object` plus the integration test
|
|
383
|
+
payloads.
|
|
384
|
+
3. **STILL OPEN, cosmetic only.** Whether the Instagram carousel reads acceptably for
|
|
385
|
+
a plain option menu, whose cards are titled `"Options 1 to 3"` because a
|
|
386
|
+
generic-template card requires a title and a menu has no natural one. Mobile-only
|
|
387
|
+
question: both of Instagram's interactive surfaces render on mobile alone, which is
|
|
388
|
+
why the renderer always numbers the options in the body as well. Nothing is broken
|
|
389
|
+
on desktop either way, so this cannot cause a failure, only an ugly card. If it
|
|
390
|
+
reads badly, override the ladder in `Instagram::Renderer` so above 13 choices goes
|
|
391
|
+
straight to numbered text.
|