1 """Slack platform for notify component."""
3 from __future__
import annotations
8 from typing
import Any, TypedDict
9 from urllib.parse
import urlparse
11 from aiohttp
import BasicAuth, FormData
12 from aiohttp.client_exceptions
import ClientError
13 from slack
import WebClient
14 from slack.errors
import SlackApiError
15 import voluptuous
as vol
21 BaseNotificationService,
42 _LOGGER = logging.getLogger(__name__)
44 FILE_PATH_SCHEMA = vol.Schema({vol.Required(CONF_PATH): cv.isfile})
46 FILE_URL_SCHEMA = vol.Schema(
48 vol.Required(ATTR_URL): cv.url,
49 vol.Inclusive(ATTR_USERNAME,
"credentials"): cv.string,
50 vol.Inclusive(ATTR_PASSWORD,
"credentials"): cv.string,
54 DATA_FILE_SCHEMA = vol.Schema(
56 vol.Required(ATTR_FILE): vol.Any(FILE_PATH_SCHEMA, FILE_URL_SCHEMA),
57 vol.Optional(ATTR_THREAD_TS): cv.string,
61 DATA_TEXT_ONLY_SCHEMA = vol.Schema(
63 vol.Optional(ATTR_USERNAME): cv.string,
64 vol.Optional(ATTR_ICON): cv.string,
65 vol.Optional(ATTR_BLOCKS): list,
66 vol.Optional(ATTR_BLOCKS_TEMPLATE): list,
67 vol.Optional(ATTR_THREAD_TS): cv.string,
71 DATA_SCHEMA = vol.All(
72 cv.ensure_list, [vol.Any(DATA_FILE_SCHEMA, DATA_TEXT_ONLY_SCHEMA)]
77 """Type for auth request data."""
83 """Type for form data, file upload."""
94 """Type for message data."""
108 discovery_info: DiscoveryInfoType |
None =
None,
109 ) -> SlackNotificationService |
None:
110 """Set up the Slack notification service."""
114 discovery_info[SLACK_DATA][DATA_CLIENT],
122 """Return the filename of a passed URL."""
123 parsed_url = urlparse(url)
124 return os.path.basename(parsed_url.path)
129 """Remove any # symbols from a channel list."""
130 return [channel.lstrip(
"#")
for channel
in channel_list]
134 """Define the Slack notification logic."""
140 config: dict[str, str],
153 thread_ts: str |
None,
155 """Upload a local file (with message) to Slack."""
156 if not self.
_hass_hass.config.is_allowed_path(path):
157 _LOGGER.error(
"Path does not exist or is not allowed: %s", path)
160 parsed_url = urlparse(path)
161 filename = os.path.basename(parsed_url.path)
164 await self.
_client_client.files_upload(
165 channels=
",".join(targets),
168 initial_comment=message,
169 title=title
or filename,
170 thread_ts=thread_ts
or "",
172 except (SlackApiError, ClientError)
as err:
173 _LOGGER.error(
"Error while uploading file-based message: %r", err)
181 thread_ts: str |
None,
183 username: str |
None =
None,
184 password: str |
None =
None,
186 """Upload a remote file (with message) to Slack.
188 Note that we bypass the python-slackclient WebClient and use aiohttp directly,
189 as the former would require us to download the entire remote file into memory
190 first before uploading it to Slack.
192 if not self.
_hass_hass.config.is_allowed_external_url(url):
193 _LOGGER.error(
"URL is not allowed: %s", url)
197 session = aiohttp_client.async_get_clientsession(self.
_hass_hass)
199 kwargs: AuthDictT = {}
200 if username
and password
is not None:
201 kwargs = {
"auth": BasicAuth(username, password=password)}
203 resp = await session.request(
"get", url, **kwargs)
206 resp.raise_for_status()
207 except ClientError
as err:
208 _LOGGER.error(
"Error while retrieving %s: %r", url, err)
211 form_data: FormDataT = {
212 "channels":
",".join(targets),
213 "filename": filename,
214 "initial_comment": message,
215 "title": title
or filename,
216 "token": self.
_client_client.token,
220 form_data[
"thread_ts"] = thread_ts
222 data = FormData(form_data, charset=
"utf-8")
223 data.add_field(
"file", resp.content, filename=filename)
226 await session.post(
"https://slack.com/api/files.upload", data=data)
227 except ClientError
as err:
228 _LOGGER.error(
"Error while uploading file message: %r", err)
235 thread_ts: str |
None,
237 username: str |
None =
None,
238 icon: str |
None =
None,
239 blocks: Any |
None =
None,
241 """Send a text-only message."""
242 message_dict: MessageT = {
"link_names":
True,
"text": message}
245 message_dict[
"username"] = username
248 if icon.lower().startswith((
"http://",
"https://")):
249 message_dict[
"icon_url"] = icon
251 message_dict[
"icon_emoji"] = icon
254 message_dict[
"blocks"] = blocks
257 message_dict[
"thread_ts"] = thread_ts
260 target: self.
_client_client.chat_postMessage(**message_dict, channel=target)
261 for target
in targets
264 results = await asyncio.gather(*tasks.values(), return_exceptions=
True)
265 for target, result
in zip(tasks, results, strict=
False):
266 if isinstance(result, SlackApiError):
268 "There was a Slack API error while sending to %s: %r",
272 elif isinstance(result, ClientError):
273 _LOGGER.error(
"Error while sending message to %s: %r", target, result)
276 """Send a message to Slack."""
277 data = kwargs.get(ATTR_DATA)
or {}
281 except vol.Invalid
as err:
282 _LOGGER.error(
"Invalid message data: %s", err)
285 title = kwargs.get(ATTR_TITLE)
287 kwargs.get(ATTR_TARGET, [self.
_config_config[CONF_DEFAULT_CHANNEL]])
291 if ATTR_FILE
not in data:
292 if ATTR_BLOCKS_TEMPLATE
in data:
293 value = cv.template_complex(data[ATTR_BLOCKS_TEMPLATE])
294 blocks = template.render_complex(value)
295 elif ATTR_BLOCKS
in data:
296 blocks = data[ATTR_BLOCKS]
304 username=data.get(ATTR_USERNAME, self.
_config_config.
get(ATTR_USERNAME)),
305 icon=data.get(ATTR_ICON, self.
_config_config.
get(ATTR_ICON)),
306 thread_ts=data.get(ATTR_THREAD_TS),
311 if ATTR_URL
in data[ATTR_FILE]:
313 data[ATTR_FILE][ATTR_URL],
317 thread_ts=data.get(ATTR_THREAD_TS),
318 username=data[ATTR_FILE].
get(ATTR_USERNAME),
319 password=data[ATTR_FILE].
get(ATTR_PASSWORD),
324 data[ATTR_FILE][ATTR_PATH],
328 thread_ts=data.get(ATTR_THREAD_TS),
None _async_send_text_only_message(self, list[str] targets, str message, str|None title, str|None thread_ts, *str|None username=None, str|None icon=None, Any|None blocks=None)
None __init__(self, HomeAssistant hass, WebClient client, dict[str, str] config)
None _async_send_remote_file_message(self, str url, list[str] targets, str message, str|None title, str|None thread_ts, *str|None username=None, str|None password=None)
None async_send_message(self, str message, **Any kwargs)
None _async_send_local_file_message(self, str path, list[str] targets, str message, str|None title, str|None thread_ts)
web.Response get(self, web.Request request, str config_key)
str _async_get_filename_from_url(str url)
SlackNotificationService|None async_get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
list[str] _async_sanitize_channel_names(list[str] channel_list)