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.
This commit is contained in:
agessaman
2026-05-30 17:25:08 -07:00
parent 7b15d43dbd
commit e6e950fcd8
5 changed files with 35 additions and 5 deletions
+2
View File
@@ -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
+3
View File
@@ -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:
@@ -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:
+3
View File
@@ -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
@@ -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"