Skip to content
Closed
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
7 changes: 7 additions & 0 deletions tests/tools/test_send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,12 @@ def test_private_channel_id_is_explicit(self):
def test_dm_id_is_explicit(self):
assert _parse_target_ref("slack", "D123ABCDEF")[2] is True

def test_channel_id_with_thread_ts_is_explicit(self):
chat_id, thread_id, is_explicit = _parse_target_ref("slack", "C0B0QV5434G:1777520157.319429")
assert chat_id == "C0B0QV5434G"
assert thread_id == "1777520157.319429"
assert is_explicit is True

def test_user_id_is_not_explicit(self):
"""Slack user IDs (U...) and workspace IDs (W...) are NOT explicit send
targets. chat.postMessage rejects them — a DM must be opened first via
Expand All @@ -875,6 +881,7 @@ def test_lowercase_or_short_id_is_not_explicit(self):
assert _parse_target_ref("slack", "c0b0qv5434g")[2] is False
assert _parse_target_ref("slack", "C123")[2] is False
assert _parse_target_ref("slack", "X0B0QV5434G")[2] is False
assert _parse_target_ref("slack", "C0B0QV5434G:not-a-ts")[2] is False

def test_slack_id_not_explicit_for_other_platforms(self):
assert _parse_target_ref("discord", "C0B0QV5434G")[2] is False
Expand Down
10 changes: 6 additions & 4 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# because the API requires a conversation ID. To DM a user you must first call
# conversations.open to obtain a D... ID. Without this gate, Slack IDs fall
# through to channel-name resolution, which only matches by name and fails.
_SLACK_TARGET_RE = re.compile(r"^\s*([CGD][A-Z0-9]{8,})\s*$")
_SLACK_TARGET_RE = re.compile(r"^\s*([CGD][A-Z0-9]{8,})(?::([0-9]+(?:\.[0-9]+)?))?\s*$")
_WEIXIN_TARGET_RE = re.compile(r"^\s*((?:wxid|gh|v\d+|wm|wb)_[A-Za-z0-9_-]+|[A-Za-z0-9._-]+@chatroom|filehelper)\s*$")
_YUANBAO_TARGET_RE = re.compile(r"^\s*((?:group|direct):[^:]+)\s*$")
# Discord snowflake IDs are numeric, same regex pattern as Telegram topic targets.
Expand Down Expand Up @@ -338,7 +338,7 @@ def _parse_target_ref(platform_name: str, target_ref: str):
if platform_name == "slack":
match = _SLACK_TARGET_RE.fullmatch(target_ref)
if match:
return match.group(1), None, True
return match.group(1), match.group(2), True
if platform_name == "weixin":
match = _WEIXIN_TARGET_RE.fullmatch(target_ref)
if match:
Expand Down Expand Up @@ -589,7 +589,7 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
last_result = None
for chunk in chunks:
if platform == Platform.SLACK:
result = await _send_slack(pconfig.token, chat_id, chunk)
result = await _send_slack(pconfig.token, chat_id, chunk, thread_id=thread_id)
elif platform == Platform.WHATSAPP:
result = await _send_whatsapp(pconfig.extra, chat_id, chunk)
elif platform == Platform.SIGNAL:
Expand Down Expand Up @@ -980,7 +980,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non
return _error(f"Discord send failed: {e}")


async def _send_slack(token, chat_id, message):
async def _send_slack(token, chat_id, message, thread_id=None):
"""Send via Slack Web API."""
try:
import aiohttp
Expand All @@ -994,6 +994,8 @@ async def _send_slack(token, chat_id, message):
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30), **_sess_kw) as session:
payload = {"channel": chat_id, "text": message, "mrkdwn": True}
if thread_id:
payload["thread_ts"] = str(thread_id)
async with session.post(url, headers=headers, json=payload, **_req_kw) as resp:
data = await resp.json()
if data.get("ok"):
Expand Down