Merge origin/main into hermes/hermes-7ef7cb6a
This commit is contained in:
commit
8ac5baf2d8
3 changed files with 119 additions and 14 deletions
|
|
@ -605,10 +605,30 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
logger.debug("Could not fetch reply-to message: %s", e)
|
logger.debug("Could not fetch reply-to message: %s", e)
|
||||||
|
|
||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
msg = await channel.send(
|
chunk_reference = reference if i == 0 else None
|
||||||
content=chunk,
|
try:
|
||||||
reference=reference if i == 0 else None,
|
msg = await channel.send(
|
||||||
)
|
content=chunk,
|
||||||
|
reference=chunk_reference,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
err_text = str(e)
|
||||||
|
if (
|
||||||
|
chunk_reference is not None
|
||||||
|
and "error code: 50035" in err_text
|
||||||
|
and "Cannot reply to a system message" in err_text
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"[%s] Reply target %s is a Discord system message; retrying send without reply reference",
|
||||||
|
self.name,
|
||||||
|
reply_to,
|
||||||
|
)
|
||||||
|
msg = await channel.send(
|
||||||
|
content=chunk,
|
||||||
|
reference=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
message_ids.append(str(msg.id))
|
message_ids.append(str(msg.id))
|
||||||
|
|
||||||
return SendResult(
|
return SendResult(
|
||||||
|
|
|
||||||
|
|
@ -2142,20 +2142,22 @@ def setup_gateway(config: dict):
|
||||||
print_info(" • Create an App-Level Token with 'connections:write' scope")
|
print_info(" • Create an App-Level Token with 'connections:write' scope")
|
||||||
print_info(" 3. Add Bot Token Scopes: Features → OAuth & Permissions")
|
print_info(" 3. Add Bot Token Scopes: Features → OAuth & Permissions")
|
||||||
print_info(" Required scopes: chat:write, app_mentions:read,")
|
print_info(" Required scopes: chat:write, app_mentions:read,")
|
||||||
print_info(" channels:history, channels:read, groups:history,")
|
print_info(" channels:history, channels:read, im:history,")
|
||||||
print_info(" im:history, im:read, im:write, users:read, files:write")
|
print_info(" im:read, im:write, users:read, files:write")
|
||||||
|
print_info(" Optional for private channels: groups:history")
|
||||||
print_info(" 4. Subscribe to Events: Features → Event Subscriptions → Enable")
|
print_info(" 4. Subscribe to Events: Features → Event Subscriptions → Enable")
|
||||||
print_info(" Required events: message.im, message.channels,")
|
print_info(" Required events: message.im, message.channels, app_mention")
|
||||||
print_info(" message.groups, app_mention")
|
print_info(" Optional for private channels: message.groups")
|
||||||
print_warning(" ⚠ Without message.channels/message.groups events,")
|
print_warning(" ⚠ Without message.channels the bot will ONLY work in DMs,")
|
||||||
print_warning(" the bot will ONLY work in DMs, not channels!")
|
print_warning(" not public channels.")
|
||||||
print_info(" 5. Install to Workspace: Settings → Install App")
|
print_info(" 5. Install to Workspace: Settings → Install App")
|
||||||
|
print_info(" 6. Reinstall the app after any scope or event changes")
|
||||||
print_info(
|
print_info(
|
||||||
" 6. After installing, invite the bot to channels: /invite @YourBot"
|
" 7. After installing, invite the bot to channels: /invite @YourBot"
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
print_info(
|
print_info(
|
||||||
" Full guide: https://hermes-agent.ai/docs/user-guide/messaging/slack"
|
" Full guide: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack/"
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
bot_token = prompt("Slack Bot Token (xoxb-...)", password=True)
|
bot_token = prompt("Slack Bot Token (xoxb-...)", password=True)
|
||||||
|
|
@ -2173,14 +2175,17 @@ def setup_gateway(config: dict):
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
allowed_users = prompt(
|
allowed_users = prompt(
|
||||||
"Allowed user IDs (comma-separated, leave empty for open access)"
|
"Allowed user IDs (comma-separated, leave empty to deny everyone except paired users)"
|
||||||
)
|
)
|
||||||
if allowed_users:
|
if allowed_users:
|
||||||
save_env_value("SLACK_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
save_env_value("SLACK_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||||
print_success("Slack allowlist configured")
|
print_success("Slack allowlist configured")
|
||||||
else:
|
else:
|
||||||
|
print_warning(
|
||||||
|
"⚠️ No Slack allowlist set - unpaired users will be denied by default."
|
||||||
|
)
|
||||||
print_info(
|
print_info(
|
||||||
"⚠️ No allowlist set - anyone in your workspace can use the bot!"
|
" Set SLACK_ALLOW_ALL_USERS=true or GATEWAY_ALLOW_ALL_USERS=true only if you intentionally want open workspace access."
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── WhatsApp ──
|
# ── WhatsApp ──
|
||||||
|
|
|
||||||
80
tests/gateway/test_discord_send.py
Normal file
80
tests/gateway/test_discord_send.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gateway.config import PlatformConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_discord_mock():
|
||||||
|
if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"):
|
||||||
|
return
|
||||||
|
|
||||||
|
discord_mod = MagicMock()
|
||||||
|
discord_mod.Intents.default.return_value = MagicMock()
|
||||||
|
discord_mod.Client = MagicMock
|
||||||
|
discord_mod.File = MagicMock
|
||||||
|
discord_mod.DMChannel = type("DMChannel", (), {})
|
||||||
|
discord_mod.Thread = type("Thread", (), {})
|
||||||
|
discord_mod.ForumChannel = type("ForumChannel", (), {})
|
||||||
|
discord_mod.ui = SimpleNamespace(View=object, button=lambda *a, **k: (lambda fn: fn), Button=object)
|
||||||
|
discord_mod.ButtonStyle = SimpleNamespace(success=1, primary=2, danger=3, green=1, blurple=2, red=3)
|
||||||
|
discord_mod.Color = SimpleNamespace(orange=lambda: 1, green=lambda: 2, blue=lambda: 3, red=lambda: 4)
|
||||||
|
discord_mod.Interaction = object
|
||||||
|
discord_mod.Embed = MagicMock
|
||||||
|
discord_mod.app_commands = SimpleNamespace(
|
||||||
|
describe=lambda **kwargs: (lambda fn: fn),
|
||||||
|
choices=lambda **kwargs: (lambda fn: fn),
|
||||||
|
Choice=lambda **kwargs: SimpleNamespace(**kwargs),
|
||||||
|
)
|
||||||
|
|
||||||
|
ext_mod = MagicMock()
|
||||||
|
commands_mod = MagicMock()
|
||||||
|
commands_mod.Bot = MagicMock
|
||||||
|
ext_mod.commands = commands_mod
|
||||||
|
|
||||||
|
sys.modules.setdefault("discord", discord_mod)
|
||||||
|
sys.modules.setdefault("discord.ext", ext_mod)
|
||||||
|
sys.modules.setdefault("discord.ext.commands", commands_mod)
|
||||||
|
|
||||||
|
|
||||||
|
_ensure_discord_mock()
|
||||||
|
|
||||||
|
from gateway.platforms.discord import DiscordAdapter # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_retries_without_reference_when_reply_target_is_system_message():
|
||||||
|
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***"))
|
||||||
|
|
||||||
|
ref_msg = SimpleNamespace(id=99)
|
||||||
|
sent_msg = SimpleNamespace(id=1234)
|
||||||
|
send_calls = []
|
||||||
|
|
||||||
|
async def fake_send(*, content, reference=None):
|
||||||
|
send_calls.append({"content": content, "reference": reference})
|
||||||
|
if len(send_calls) == 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
"400 Bad Request (error code: 50035): Invalid Form Body\n"
|
||||||
|
"In message_reference: Cannot reply to a system message"
|
||||||
|
)
|
||||||
|
return sent_msg
|
||||||
|
|
||||||
|
channel = SimpleNamespace(
|
||||||
|
fetch_message=AsyncMock(return_value=ref_msg),
|
||||||
|
send=AsyncMock(side_effect=fake_send),
|
||||||
|
)
|
||||||
|
adapter._client = SimpleNamespace(
|
||||||
|
get_channel=lambda _chat_id: channel,
|
||||||
|
fetch_channel=AsyncMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await adapter.send("555", "hello", reply_to="99")
|
||||||
|
|
||||||
|
assert result.success is True
|
||||||
|
assert result.message_id == "1234"
|
||||||
|
assert channel.fetch_message.await_count == 1
|
||||||
|
assert channel.send.await_count == 2
|
||||||
|
assert send_calls[0]["reference"] is ref_msg
|
||||||
|
assert send_calls[1]["reference"] is None
|
||||||
Loading…
Add table
Add a link
Reference in a new issue