feat: interactive messages — action handlers for buttons and menus#33
Conversation
Add action-handler routing for Mattermost interactive messages (buttons, menus, dialogs). When a user clicks an action on an interactive message, Mattermost POSTs to a webhook; this system routes those payloads to registered handlers. - InteractiveAction DTO wrapping webhook payload with typed accessors - InteractiveActionHandler abstract base class with container DI - InteractiveActionRouter for route-style handler registration - InteractiveActionResponse builder (update post, ephemeral, props) - InteractiveActionController webhook endpoint auto-registered - Manager and Facade integration (Mattermost::interactive()) - Config section for route and middleware customization - Full Pest test coverage (describe/it BDD) Closes #8
There was a problem hiding this comment.
Blocking: Interactive webhooks use application/x-www-form-urlencoded with payload param holding JSON-encoded string — not direct JSON body. $request->all() gives ['payload' => '{...}']; DTO then fails as actionId() etc. expect flat payload. Real payload nests actions under top-level 'action' => [{ 'id' => '...', ... }], not $payload['action'] scalar.
Fix controller:
$payloadString = $request->input('payload');
$payload = json_decode($payloadString ?? '', true) ?? [];
if (json_last_error() !== JSON_ERROR_NONE) {
return new JsonResponse(['ephemeral_text' => 'Invalid payload'], 400);
}
$action = InteractiveAction::fromPayload($payload);Fix DTO accessors (e.g. actionId()): Extract from $payload['action'][0]['id'] ?? '' etc. Update docblocks (type() is top-level payload type like 'interactive_message', not element type).
Fix tests: Use $this->post(..., ['payload' => json_encode($fixture)]); update fixtures to nest action => [{'id' => '...'}]; drop bogus context['action'] expectation.
Mirrors slash commands well otherwise (which use flat form data [from: GitHubReadFile conduit-ui/mattermost:src/SlashCommands/SlashCommandController.php]). Full Pest coverage + Pint/Pest/PHPStan/Rector green is solid.
PR #33's Lexi review (CHANGES_REQUESTED, posted 6m after the merge) flagged that InteractiveAction::actionId() and value() were reading fields that don't exist in real Mattermost webhook payloads. The bug was real but the format diagnosis in the review was off — Mattermost sends JSON (not Slack-style form-urlencoded with a `payload` field), but it does NOT put `action` or `value` at the top level. Both come back inside the integrator-supplied `context` object. The DTO's tests passed because MattermostFixtures::fakeButtonClick() fabricated both layouts (top-level shortcuts AND the real `context` key). In production the top-level keys are absent, so actionId() returned '' and the router never matched any registered handler. - src/Interactive/InteractiveAction.php: actionId() and value() now read from `context.action` and `context.value` respectively, with doc-blocks explaining the convention (integrator stuffs the action name into the button definition's `integration.context.action`). - src/Testing/MattermostFixtures.php: drop the spurious top-level `action` and `value` shortcuts; nest both inside `context` to match what real Mattermost sends. - tests/Unit/Testing/MattermostFixturesTest.php: updated the fakeButtonClick assertion to verify the new shape and explicitly assert no top-level `action`/`value` keys. Controller is unchanged — `$request->all()` works fine for MM's real JSON body. 341 passed (14 integration skipped); pint, phpstan level 8, rector all clean.
Summary
register(actionId, handler|closure)anddispatch()— mirrorsSlashCommandRouterupdate()(replace original post text),props()(replace attachments), andephemeral()(whisper to acting user)mattermost/interactivevia configMattermost::interactive('action-id', Handler::class)andMattermost::interactiveActionRouter()mattermost.interactivewith route and middleware optionsTest plan
vendor/bin/pint --testpassesvendor/bin/pest --compact— 302 passed (all new Interactive tests green)vendor/bin/phpstan analyse— no errors at level 8vendor/bin/rector process --dry-run— no changes suggestedCloses #8
Demo: http://localhost:8065/jordan-partridge-enterprises-llc/pl/fwp7wfnq67bdmqkkp1jp3x874y