Skip to content
Open
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
2 changes: 2 additions & 0 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,8 @@ def load_gateway_config() -> GatewayConfig:
bridged["reply_in_thread"] = platform_cfg["reply_in_thread"]
if "require_mention" in platform_cfg:
bridged["require_mention"] = platform_cfg["require_mention"]
if plat == Platform.SLACK and "unfurl" in platform_cfg:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current main moved Slack-specific YAML translation into the Slack plugin hook (plugins/platforms/slack/adapter.py::_apply_yaml_config); port this shorthand bridge there rather than adding a new Slack exception back to gateway core.

bridged["unfurl"] = platform_cfg["unfurl"]
if plat == Platform.TELEGRAM and "allowed_chats" in platform_cfg:
bridged["allowed_chats"] = platform_cfg["allowed_chats"]
if plat == Platform.TELEGRAM and "group_allowed_chats" in platform_cfg:
Expand Down
7 changes: 7 additions & 0 deletions gateway/platforms/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,13 +790,20 @@ async def send(
# reply_broadcast: also post thread replies to the main channel.
# Controlled via platform config: gateway.slack.reply_broadcast
broadcast = self.config.extra.get("reply_broadcast", False)
# When gateway.slack.unfurl is false, suppress Slack's link/media
# previews so posted URLs don't expand into preview cards. Defaults
# to true, preserving Slack's native unfurling behavior.
unfurl = self.config.extra.get("unfurl", True)

for i, chunk in enumerate(chunks):
kwargs = {
"channel": chat_id,
"text": chunk,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This adapter path was moved to plugins/platforms/slack/adapter.py by 560010547; port the send change there and also cover that plugin's _standalone_send() cron delivery path.

"mrkdwn": True,
}
if not unfurl:
kwargs["unfurl_links"] = False
kwargs["unfurl_media"] = False
if thread_ts:
kwargs["thread_ts"] = thread_ts
# Only broadcast the first chunk of the first reply
Expand Down
47 changes: 46 additions & 1 deletion tests/gateway/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import pytest

from gateway.config import Platform, PlatformConfig
from gateway.config import Platform, PlatformConfig, load_gateway_config
from gateway.platforms.base import (
MessageEvent,
MessageType,
Expand Down Expand Up @@ -2458,6 +2458,27 @@ async def test_send_explicitly_enables_mrkdwn(self, adapter):
kwargs = adapter._app.client.chat_postMessage.call_args.kwargs
assert kwargs.get("mrkdwn") is True

@pytest.mark.asyncio
async def test_send_disables_url_previews_when_unfurl_off(self, adapter):
"""With gateway.slack.unfurl=false, posts must suppress link/media
unfurling so URLs don't expand into noisy preview cards."""
adapter.config.extra["unfurl"] = False
adapter._app.client.chat_postMessage = AsyncMock(return_value={"ts": "ts1"})
await adapter.send("C123", "see https://example.com/article")
kwargs = adapter._app.client.chat_postMessage.call_args.kwargs
assert kwargs.get("unfurl_links") is False
assert kwargs.get("unfurl_media") is False

@pytest.mark.asyncio
async def test_send_preserves_url_previews_by_default(self, adapter):
"""Default behavior leaves Slack's unfurling untouched — no unfurl
flags are sent, so Slack applies its native preview behavior."""
adapter._app.client.chat_postMessage = AsyncMock(return_value={"ts": "ts1"})
await adapter.send("C123", "see https://example.com/article")
kwargs = adapter._app.client.chat_postMessage.call_args.kwargs
assert "unfurl_links" not in kwargs
assert "unfurl_media" not in kwargs

@pytest.mark.asyncio
async def test_send_does_not_double_escape_entities(self, adapter):
"""Pre-escaped & in sent messages must not become &."""
Expand Down Expand Up @@ -3177,3 +3198,27 @@ async def test_no_contextvar_does_not_match_any_context(self, adapter):
# the normal single-user case; the ContextVar path is the precise one.
# The key invariant is: when the ContextVar IS set, it matches exactly.
assert ctx is not None # fallback path finds the entry


class TestUnfurlConfigBridging:
"""gateway.slack.unfurl must bridge from YAML into PlatformConfig.extra so
the send path can read it — guards the allowlist wiring in
load_gateway_config (a non-allowlisted key would be silently dropped)."""

def _write_config(self, tmp_path, content: str):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(content, encoding="utf-8")
return hermes_home

def test_top_level_unfurl_false_bridges_to_extra(self, tmp_path, monkeypatch):
hermes_home = self._write_config(tmp_path, "slack:\n unfurl: false\n")
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
cfg = load_gateway_config()
assert cfg.platforms[Platform.SLACK].extra.get("unfurl") is False

def test_unfurl_absent_when_not_configured(self, tmp_path, monkeypatch):
hermes_home = self._write_config(tmp_path, "slack:\n require_mention: true\n")
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
cfg = load_gateway_config()
assert "unfurl" not in cfg.platforms[Platform.SLACK].extra
20 changes: 20 additions & 0 deletions website/docs/user-guide/messaging/slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,26 @@ platforms:
| `platforms.slack.extra.reply_in_thread` | `true` | When `false`, channel messages get direct replies instead of threads. Messages inside existing threads still reply in-thread. |
| `platforms.slack.extra.reply_broadcast` | `false` | When `true`, thread replies are also posted to the main channel. Only the first chunk is broadcast. |

### Link Previews

By default Slack expands posted URLs into preview cards ("unfurling"). For bots
that post a lot of links — news feeds, digests, alerts — this is often noise.
Set `unfurl: false` to suppress link and media previews on the bot's own posts:

```yaml
platforms:
slack:
extra:
# Suppress Slack link/media previews on the bot's posts (default: true)
unfurl: false
```

The top-level `slack.unfurl` shorthand is also accepted.

| Key | Default | Description |
|-----|---------|-------------|
| `platforms.slack.extra.unfurl` | `true` | When `false`, the bot's posts disable Slack's `unfurl_links`/`unfurl_media`, so posted URLs don't expand into preview cards. |

### Session Isolation

```yaml
Expand Down