Skip to content

Commit

Permalink
Add Slack thread/reply support (#93384)
Browse files Browse the repository at this point in the history
  • Loading branch information
FletcherAU committed Sep 21, 2023
1 parent e4742c0 commit 11c4c37
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 6 deletions.
4 changes: 2 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1145,8 +1145,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/skybell/ @tkdrob
/tests/components/skybell/ @tkdrob
/homeassistant/components/slack/ @tkdrob
/tests/components/slack/ @tkdrob
/homeassistant/components/slack/ @tkdrob @fletcherau
/tests/components/slack/ @tkdrob @fletcherau
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
/tests/components/sleepiq/ @mfugate1 @kbickar
/homeassistant/components/slide/ @ualex73
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/slack/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ATTR_URL = "url"
ATTR_USERNAME = "username"
ATTR_USER_ID = "user_id"
ATTR_THREAD_TS = "thread_ts"

CONF_DEFAULT_CHANNEL = "default_channel"

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/slack/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "slack",
"name": "Slack",
"codeowners": ["@tkdrob"],
"codeowners": ["@tkdrob", "@fletcherau"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/slack",
"integration_type": "service",
Expand Down
29 changes: 26 additions & 3 deletions homeassistant/components/slack/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ATTR_FILE,
ATTR_PASSWORD,
ATTR_PATH,
ATTR_THREAD_TS,
ATTR_URL,
ATTR_USERNAME,
CONF_DEFAULT_CHANNEL,
Expand All @@ -50,7 +51,10 @@
)

DATA_FILE_SCHEMA = vol.Schema(
{vol.Required(ATTR_FILE): vol.Any(FILE_PATH_SCHEMA, FILE_URL_SCHEMA)}
{
vol.Required(ATTR_FILE): vol.Any(FILE_PATH_SCHEMA, FILE_URL_SCHEMA),
vol.Optional(ATTR_THREAD_TS): cv.string,
}
)

DATA_TEXT_ONLY_SCHEMA = vol.Schema(
Expand All @@ -59,6 +63,7 @@
vol.Optional(ATTR_ICON): cv.string,
vol.Optional(ATTR_BLOCKS): list,
vol.Optional(ATTR_BLOCKS_TEMPLATE): list,
vol.Optional(ATTR_THREAD_TS): cv.string,
}
)

Expand All @@ -73,14 +78,15 @@ class AuthDictT(TypedDict, total=False):
auth: BasicAuth


class FormDataT(TypedDict):
class FormDataT(TypedDict, total=False):
"""Type for form data, file upload."""

channels: str
filename: str
initial_comment: str
title: str
token: str
thread_ts: str # Optional key


class MessageT(TypedDict, total=False):
Expand All @@ -92,6 +98,7 @@ class MessageT(TypedDict, total=False):
icon_url: str # Optional key
icon_emoji: str # Optional key
blocks: list[Any] # Optional key
thread_ts: str # Optional key


async def async_get_service(
Expand Down Expand Up @@ -142,6 +149,7 @@ async def _async_send_local_file_message(
targets: list[str],
message: str,
title: str | None,
thread_ts: str | None,
) -> None:
"""Upload a local file (with message) to Slack."""
if not self._hass.config.is_allowed_path(path):
Expand All @@ -158,6 +166,7 @@ async def _async_send_local_file_message(
filename=filename,
initial_comment=message,
title=title or filename,
thread_ts=thread_ts,
)
except (SlackApiError, ClientError) as err:
_LOGGER.error("Error while uploading file-based message: %r", err)
Expand All @@ -168,6 +177,7 @@ async def _async_send_remote_file_message(
targets: list[str],
message: str,
title: str | None,
thread_ts: str | None,
*,
username: str | None = None,
password: str | None = None,
Expand Down Expand Up @@ -205,6 +215,9 @@ async def _async_send_remote_file_message(
"token": self._client.token,
}

if thread_ts:
form_data["thread_ts"] = thread_ts

data = FormData(form_data, charset="utf-8")
data.add_field("file", resp.content, filename=filename)

Expand All @@ -218,6 +231,7 @@ async def _async_send_text_only_message(
targets: list[str],
message: str,
title: str | None,
thread_ts: str | None,
*,
username: str | None = None,
icon: str | None = None,
Expand All @@ -238,6 +252,9 @@ async def _async_send_text_only_message(
if blocks:
message_dict["blocks"] = blocks

if thread_ts:
message_dict["thread_ts"] = thread_ts

tasks = {
target: self._client.chat_postMessage(**message_dict, channel=target)
for target in targets
Expand Down Expand Up @@ -286,6 +303,7 @@ async def async_send_message(self, message: str, **kwargs: Any) -> None:
title,
username=data.get(ATTR_USERNAME, self._config.get(ATTR_USERNAME)),
icon=data.get(ATTR_ICON, self._config.get(ATTR_ICON)),
thread_ts=data.get(ATTR_THREAD_TS),
blocks=blocks,
)

Expand All @@ -296,11 +314,16 @@ async def async_send_message(self, message: str, **kwargs: Any) -> None:
targets,
message,
title,
thread_ts=data.get(ATTR_THREAD_TS),
username=data[ATTR_FILE].get(ATTR_USERNAME),
password=data[ATTR_FILE].get(ATTR_PASSWORD),
)

# Message Type 3: A message that uploads a local file
return await self._async_send_local_file_message(
data[ATTR_FILE][ATTR_PATH], targets, message, title
data[ATTR_FILE][ATTR_PATH],
targets,
message,
title,
thread_ts=data.get(ATTR_THREAD_TS),
)
16 changes: 16 additions & 0 deletions tests/components/slack/test_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from homeassistant.components import notify
from homeassistant.components.slack import DOMAIN
from homeassistant.components.slack.notify import (
ATTR_THREAD_TS,
CONF_DEFAULT_CHANNEL,
SlackNotificationService,
)
Expand Down Expand Up @@ -93,3 +94,18 @@ async def test_message_icon_url_overrides_default() -> None:
mock_fn.assert_called_once()
_, kwargs = mock_fn.call_args
assert kwargs["icon_url"] == expected_icon


async def test_message_as_reply() -> None:
"""Tests that a message pointer will be passed to Slack if specified."""
mock_client = Mock()
mock_client.chat_postMessage = AsyncMock()
service = SlackNotificationService(None, mock_client, CONF_DATA)

expected_ts = "1624146685.064129"
await service.async_send_message("test", data={ATTR_THREAD_TS: expected_ts})

mock_fn = mock_client.chat_postMessage
mock_fn.assert_called_once()
_, kwargs = mock_fn.call_args
assert kwargs["thread_ts"] == expected_ts

0 comments on commit 11c4c37

Please sign in to comment.