Skip to content

feat: interactive messages — action handlers for buttons and menus#33

Merged
jordanpartridge merged 1 commit into
mainfrom
feat/8-interactive-messages
Apr 27, 2026
Merged

feat: interactive messages — action handlers for buttons and menus#33
jordanpartridge merged 1 commit into
mainfrom
feat/8-interactive-messages

Conversation

@jordanpartridge
Copy link
Copy Markdown
Contributor

Summary

  • InteractiveAction DTO wrapping Mattermost's interactive webhook payload with typed accessors (action ID, user, channel, post, context, type, trigger ID)
  • InteractiveActionHandler abstract base class resolved from the container for constructor DI
  • InteractiveActionRouter with route-style register(actionId, handler|closure) and dispatch() — mirrors SlashCommandRouter
  • InteractiveActionResponse fluent builder supporting update() (replace original post text), props() (replace attachments), and ephemeral() (whisper to acting user)
  • InteractiveActionController webhook endpoint auto-registered at mattermost/interactive via config
  • Manager + Facade integration: Mattermost::interactive('action-id', Handler::class) and Mattermost::interactiveActionRouter()
  • Config section mattermost.interactive with route and middleware options
  • Full Pest test coverage (describe/it BDD) — DTO, response builder, router, and controller HTTP tests

Test plan

  • vendor/bin/pint --test passes
  • vendor/bin/pest --compact — 302 passed (all new Interactive tests green)
  • vendor/bin/phpstan analyse — no errors at level 8
  • vendor/bin/rector process --dry-run — no changes suggested

Closes #8

Demo: http://localhost:8065/jordan-partridge-enterprises-llc/pl/fwp7wfnq67bdmqkkp1jp3x874y

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
@jordanpartridge jordanpartridge merged commit 5abe9d0 into main Apr 27, 2026
4 checks passed
@jordanpartridge jordanpartridge deleted the feat/8-interactive-messages branch April 27, 2026 01:34
Copy link
Copy Markdown

@lexi-chief-of-staff lexi-chief-of-staff Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

jordanpartridge added a commit that referenced this pull request Apr 27, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Interactive messages — action handlers for buttons and menus

1 participant