diff --git a/reconcile/utils/slack_api.py b/reconcile/utils/slack_api.py index 53b148cd5f..d5a92302fb 100644 --- a/reconcile/utils/slack_api.py +++ b/reconcile/utils/slack_api.py @@ -13,6 +13,7 @@ Protocol, Union, ) +from urllib.parse import urlparse from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -224,7 +225,7 @@ def _configure_client_retry(self) -> None: self._sc.retry_handlers.append(rate_limit_handler) self._sc.retry_handlers.append(server_error_handler) - def chat_post_message(self, text: str) -> None: + def chat_post_message(self, text: str, channel_override: Optional[str] = None, thread_ts: Optional[str] = None) -> None: """ Try to send a chat message into a channel. If the bot is not in the channel it will join the channel and send the message again. @@ -234,22 +235,23 @@ def chat_post_message(self, text: str) -> None: :raises slack_sdk.errors.SlackApiError: if unsuccessful response from Slack API, except for not_in_channel """ - if not self.channel: + channel = channel_override or self.channel + if not channel: raise ValueError( "Slack channel name must be provided when posting messages." ) def do_send(c: str, t: str) -> None: slack_request.labels("chat.postMessage", "POST").inc() - self._sc.chat_postMessage(channel=c, text=t, **self.chat_kwargs) + self._sc.chat_postMessage(channel=c, text=t, **self.chat_kwargs, thread_ts=thread_ts) try: - do_send(self.channel, text) + do_send(channel, text) except SlackApiError as e: match e.response["error"]: case "not_in_channel": - self.join_channel() - do_send(self.channel, text) + self.join_channel(channel_override=channel_override) + do_send(channel, text) # When a message is sent to #someChannel and the Slack API can't find # it, the message it provides in the exception doesn't include the # channel name. We handle that here in case the consumer has many such @@ -260,6 +262,20 @@ def do_send(c: str, t: str) -> None: case _: raise + def chat_post_message_to_thread(self, text: str, thread_url: str) -> None: + """ + Send a message to a thread + """ + # example parent message url + # https://example.slack.com/archives/C017E996GPP/p1715146351427019 + parsed_thread_url = urlparse(thread_url) + if parsed_thread_url.netloc != f"{self.workspace_name}.slack.com": + raise ValueError("Slack workspace must match thread URL.") + _, _, channel_id, p_timstamp = parsed_thread_url.path.split("/") + timstamp = p_timstamp.replace("p", "") + thread_ts = f"{timstamp[:10]}.{timstamp[10:]}" + self.chat_post_message(text, channel_override=channel_id, thread_ts=thread_ts) + def describe_usergroup( self, handle: str ) -> tuple[dict[str, str], dict[str, str], str]: @@ -274,7 +290,7 @@ def describe_usergroup( return users, channels, description - def join_channel(self) -> None: + def join_channel(self, channel_override: Optional[str] = None) -> None: """ Join a given channel if not already a member, will join self.channel @@ -282,13 +298,14 @@ def join_channel(self) -> None: Slack API :raises ValueError: if self.channel is not set """ - if not self.channel: + channel = channel_override or self.channel + if channel: raise ValueError( "Slack channel name must be provided when joining a channel." ) - channels_found = self.get_channels_by_names(self.channel) - [channel_id] = [k for k in channels_found if channels_found[k] == self.channel] + channels_found = self.get_channels_by_names(channel) + [channel_id] = [k for k in channels_found if channels_found[k] == channel] slack_request.labels("conversations.info", "GET").inc() info = self._sc.conversations_info(channel=channel_id) diff --git a/tools/qontract_cli.py b/tools/qontract_cli.py index 460f62b442..2210c252e2 100755 --- a/tools/qontract_cli.py +++ b/tools/qontract_cli.py @@ -2605,10 +2605,16 @@ def slack_usergroup(ctx, workspace, usergroup, username): slack.update_usergroup_users(ugid, users) -@root.command() +@root.group(name="slack") +@output +@click.pass_context +def slack(ctx, output): + ctx.obj["output"] = output + +@slack.command() @click.argument("message") @click.pass_context -def slack_message(ctx, message): +def message(ctx, message: str): """ Send a Slack message. """ @@ -2616,6 +2622,18 @@ def slack_message(ctx, message): slack.chat_post_message(message) +@slack.command() +@click.argument("message") +@click.argument("thread_url") +@click.pass_context +def message_thread(ctx, message: str, thread_url: str): + """ + Send a Slack message to a thread. + """ + slack = slackapi_from_queries("qontract-cli", init_usergroups=False) + slack.chat_post_message_to_thread(message, thread_url) + + @set_command.command() @click.argument("org_name") @click.argument("cluster_name")