From e6e950fcd88c3ee29dc9bdd9673fd0dc64c83ad7 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sat, 30 May 2026 17:25:08 -0700 Subject: [PATCH] feat(discord-bridge): suppress mention notifications and enhance message formatting - Implemented suppression of Discord mention notifications for bridged messages by setting `allowed_mentions` to an empty list, ensuring that mentions like `@everyone` and `@here` are displayed as plain text. - Updated documentation to reflect this change and clarify the formatting of mentions in bridged messages. - Adjusted payload structure in the Discord webhook integration to include `allowed_mentions` for better control over message parsing. - Enhanced unit tests to verify the inclusion of `allowed_mentions` in the message payload. --- docs/discord-bridge.md | 2 ++ modules/bridge_outbound.py | 3 +++ .../service_plugins/discord_bridge_service.py | 12 ++++++----- tests/test_bridge_outbound.py | 3 +++ tests/test_discord_bridge_multi_webhooks.py | 20 +++++++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/discord-bridge.md b/docs/discord-bridge.md index 611732d..d6b4a0c 100644 --- a/docs/discord-bridge.md +++ b/docs/discord-bridge.md @@ -139,6 +139,8 @@ seriously, there are some people... MeshCore @ mentions are cleaned up and bolded: `@[username]` → `**@username**` +Bridged messages never trigger Discord mention notifications (`@everyone`, `@here`, roles, or users). The webhook sets `allowed_mentions` so mesh highlights display as plain text only. + --- ## Troubleshooting diff --git a/modules/bridge_outbound.py b/modules/bridge_outbound.py index 51c4f4d..b0da2e2 100644 --- a/modules/bridge_outbound.py +++ b/modules/bridge_outbound.py @@ -28,6 +28,8 @@ except ImportError: REQUESTS_AVAILABLE = False DISCORD_WEBHOOK_PREFIX = "https://discord.com/api/webhooks/" +# Suppress @everyone/@here/role/user pings when relaying mesh text to Discord. +DISCORD_WEBHOOK_ALLOWED_MENTIONS: dict[str, list[str]] = {"parse": []} DISCORD_CONTENT_MAX = 2000 TELEGRAM_TEXT_MAX = 4096 TELEGRAM_TRUNCATE_AT = 4000 @@ -68,6 +70,7 @@ async def post_discord_webhook( payload = { "content": _truncate_discord_content(content), "username": username[:80] if username else "MeshCore", + "allowed_mentions": DISCORD_WEBHOOK_ALLOWED_MENTIONS, } if AIOHTTP_AVAILABLE: diff --git a/modules/service_plugins/discord_bridge_service.py b/modules/service_plugins/discord_bridge_service.py index 487807d..59c7765 100644 --- a/modules/service_plugins/discord_bridge_service.py +++ b/modules/service_plugins/discord_bridge_service.py @@ -35,6 +35,7 @@ except ImportError: # Import base service import contextlib +from ..bridge_outbound import DISCORD_WEBHOOK_ALLOWED_MENTIONS from ..profanity_filter import censor, contains_profanity from ..security_utils import sanitize_name from .base_service import BaseServicePlugin @@ -44,7 +45,7 @@ from .base_service import BaseServicePlugin class QueuedMessage: """Represents a message queued for Discord posting.""" webhook_url: str - payload: dict[str, str] + payload: dict[str, Any] channel_name: str retry_count: int = 0 first_queued: float = 0.0 # Timestamp when first queued @@ -450,7 +451,8 @@ class DiscordBridgeService(BaseServicePlugin): payload = { "content": message, - "username": username + "username": username, + "allowed_mentions": DISCORD_WEBHOOK_ALLOWED_MENTIONS, } if avatar_url: @@ -575,7 +577,7 @@ class DiscordBridgeService(BaseServicePlugin): self.logger.error(f"Error in message queue processor: {e}", exc_info=True) await asyncio.sleep(1.0) # Wait a bit before retrying on error - async def _post_to_webhook(self, webhook_url: str, payload: dict[str, str], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: + async def _post_to_webhook(self, webhook_url: str, payload: dict[str, Any], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: """Post message to Discord webhook. Args: @@ -601,7 +603,7 @@ class DiscordBridgeService(BaseServicePlugin): self.logger.error(f"Failed to post to Discord webhook [{channel_name}]: {e}", exc_info=True) return False - async def _post_async(self, webhook_url: str, payload: dict[str, str], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: + async def _post_async(self, webhook_url: str, payload: dict[str, Any], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: """Post to webhook using aiohttp (async). Args: @@ -653,7 +655,7 @@ class DiscordBridgeService(BaseServicePlugin): self.logger.error(f"Error posting to Discord webhook [{channel_name}]: {e}") return False - async def _post_sync(self, webhook_url: str, payload: dict[str, str], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: + async def _post_sync(self, webhook_url: str, payload: dict[str, Any], channel_name: str, queued_msg: Optional[QueuedMessage] = None) -> bool: """Post to webhook using requests library (sync fallback). Args: diff --git a/tests/test_bridge_outbound.py b/tests/test_bridge_outbound.py index 91baf2b..d4e3708 100644 --- a/tests/test_bridge_outbound.py +++ b/tests/test_bridge_outbound.py @@ -35,6 +35,8 @@ async def test_post_discord_webhook_async_success(): logger=MagicMock(), ) assert ok is True + _, kwargs = mock_session.post.call_args + assert kwargs["json"]["allowed_mentions"] == bridge_outbound.DISCORD_WEBHOOK_ALLOWED_MENTIONS @pytest.mark.asyncio @@ -85,3 +87,4 @@ async def test_post_discord_requests_fallback(): ) assert ok is True p.assert_called_once() + assert p.call_args.kwargs["json"]["allowed_mentions"] == bridge_outbound.DISCORD_WEBHOOK_ALLOWED_MENTIONS diff --git a/tests/test_discord_bridge_multi_webhooks.py b/tests/test_discord_bridge_multi_webhooks.py index 703e205..7ab225d 100644 --- a/tests/test_discord_bridge_multi_webhooks.py +++ b/tests/test_discord_bridge_multi_webhooks.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from modules.bridge_outbound import DISCORD_WEBHOOK_ALLOWED_MENTIONS from modules.service_plugins.discord_bridge_service import DiscordBridgeService @@ -62,3 +63,22 @@ async def test_multiple_webhooks_parsed_and_queued(multi_webhook_bot): assert mock_queue.await_count == 2 called_urls = {call.args[0] for call in mock_queue.await_args_list} assert called_urls == {url1, url2} + + +@pytest.mark.asyncio +async def test_queue_message_includes_allowed_mentions(multi_webhook_bot): + """Webhook payload disables Discord mention parsing (no @everyone pings).""" + url = "https://discord.com/api/webhooks/123/abc" + multi_webhook_bot.config.set("DiscordBridge", "enabled", "true") + multi_webhook_bot.config.set("DiscordBridge", "bridge.Public", url) + + service = DiscordBridgeService(multi_webhook_bot) + await service._queue_message(url, "**@everyone** alert", "Public", "Alice") + + queued = service.message_queues[url][0] + assert queued.payload["allowed_mentions"] == DISCORD_WEBHOOK_ALLOWED_MENTIONS + + +def test_format_mentions_bolds_mesh_highlight(multi_webhook_bot): + service = DiscordBridgeService(multi_webhook_bot) + assert service._format_mentions("@[everyone] hi") == "**@everyone** hi"