Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/cookbook/07-slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ Output is the CLI's piped-mode bytes in a code fence. stdout in the bot is a
pipe, so by the CLI's own contract there is no colour to strip and the bytes
match what `grapharc … | cat` prints on the host.

A successful `viz` reply carries one extra line: a *render this diagram* link.
The whole diagram is zlib-compressed into the URL fragment, which a browser
never sends to any server — mermaid.live's JavaScript renders it locally, so
following the link ships the diagram to no one.

## Slack app setup (once, ~5 minutes)

1. <https://api.slack.com/apps> → **Create New App** → *From a manifest*, pick
Expand Down
20 changes: 20 additions & 0 deletions grapharc/slack/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@

from __future__ import annotations

import base64
import json
import shlex
import zlib

from grapharc.slack.runner import CommandResult

Expand All @@ -39,6 +42,19 @@ def _truncate(body: str) -> tuple[str, int]:
return body[:MAX_FENCE_CHARS], len(body) - MAX_FENCE_CHARS


def mermaid_live_url(code: str) -> str:
"""A mermaid.live link with the whole diagram compressed into the fragment.

The editor's `#pako:` form: zlib-deflated JSON, base64url. Everything after
the `#` is a URL fragment, which a browser never sends to the server — the
site's JavaScript renders the diagram locally, so following the link ships
the diagram to no one.
"""
payload = json.dumps({"code": code, "mermaid": {"theme": "default"}})
packed = base64.urlsafe_b64encode(zlib.compress(payload.encode())).decode()
return f"https://mermaid.live/view#pako:{packed}"


def format_result(result: CommandResult) -> str:
shown = shlex.join(["grapharc", *result.argv])
if result.exit_code is None:
Expand All @@ -64,4 +80,8 @@ def format_result(result: CommandResult) -> str:
parts.append(f"_…{cut} more characters not shown._")
if len(parts) == 1:
parts.append("_(no output)_")
# `viz` prints raw Mermaid, which Slack shows as text. One extra line makes
# it a diagram: a link that renders it in the browser, locally.
if result.argv[:1] == ["viz"] and result.exit_code == 0 and result.stdout.strip():
parts.append(f"<{mermaid_live_url(result.stdout.strip())}|render this diagram>")
return "\n".join(parts)
37 changes: 36 additions & 1 deletion tests/test_slack_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from grapharc.slack.command import SlackCommandError, parse_command, usage_text
from grapharc.slack.config import SlackBotConfig, SlackConfigError
from grapharc.slack.format import MAX_FENCE_CHARS, format_result
from grapharc.slack.format import MAX_FENCE_CHARS, format_result, mermaid_live_url
from grapharc.slack.runner import CommandResult, run_command

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -147,6 +147,41 @@ def test_a_fence_in_the_output_cannot_break_out():
assert "\n```\n" not in body.rsplit("```", 1)[0]


def test_a_successful_viz_gets_a_render_link_and_the_url_round_trips():
import base64
import json
import zlib

mermaid = 'flowchart TD\n start((start)) --> n0["triage"]'
result = CommandResult(
argv=["viz", "t.jsonl", "r1"],
exit_code=0,
stdout=mermaid + "\n",
stderr="",
duration_seconds=0.1,
timeout_seconds=60,
)
message = format_result(result)
assert "mermaid.live/view#pako:" in message

packed = mermaid_live_url(mermaid).split("#pako:", 1)[1]
decoded = json.loads(zlib.decompress(base64.urlsafe_b64decode(packed)))
assert decoded["code"] == mermaid


def test_a_failed_or_non_viz_command_gets_no_render_link():
for argv, code in ((["viz", "t.jsonl", "r1"], 1), (["trace", "t.jsonl"], 0)):
result = CommandResult(
argv=argv,
exit_code=code,
stdout="flowchart TD",
stderr="",
duration_seconds=0.1,
timeout_seconds=60,
)
assert "mermaid.live" not in format_result(result)


# ---------------------------------------------------------------------------
# Config and the bot's import posture.
# ---------------------------------------------------------------------------
Expand Down
Loading