Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[qontract-cli] slack actions #4347

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
53 changes: 43 additions & 10 deletions reconcile/utils/slack_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Protocol,
Union,
)
from urllib.parse import urlparse

from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
Expand Down Expand Up @@ -224,7 +225,12 @@ 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.
Expand All @@ -234,22 +240,25 @@ 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
Expand All @@ -260,6 +269,29 @@ def do_send(c: str, t: str) -> None:
case _:
raise

def _get_channel_and_timestamp(self, url: str) -> tuple[str, str]:
# example parent message url
# https://example.slack.com/archives/C017E996GPP/p1715146351427019
parsed_url = urlparse(url)
if parsed_url.netloc != f"{self.workspace_name}.slack.com":
raise ValueError("Slack workspace must match thread URL.")
_, _, channel, p_timestamp = parsed_url.path.split("/")
timestamp = p_timestamp.replace("p", "")
ts = f"{timestamp[:10]}.{timestamp[10:]}" if "." not in timestamp else timestamp

return channel, ts

def chat_post_message_to_thread(self, text: str, thread_url: str) -> None:
"""
Send a message to a thread
"""
channel, thread_ts = self._get_channel_and_timestamp(thread_url)
self.chat_post_message(text, channel_override=channel, thread_ts=thread_ts)

def add_reaction(self, reaction: str, message_url: str) -> None:
channel, message_ts = self._get_channel_and_timestamp(message_url)
self._sc.reactions_add(channel=channel, name=reaction, timestamp=message_ts)

def describe_usergroup(
self, handle: str
) -> tuple[dict[str, str], dict[str, str], str]:
Expand All @@ -274,21 +306,22 @@ 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

:raises slack_sdk.errors.SlackApiError: if unsuccessful response from
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)
Expand Down
42 changes: 42 additions & 0 deletions tools/qontract_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,48 @@ def slack_usergroup(ctx, workspace, usergroup, username):
slack.update_usergroup_users(ugid, users)


@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 message(ctx, message: str):
"""
Send a Slack message.
"""
slack = slackapi_from_queries("qontract-cli", init_usergroups=False)
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)


@slack.command()
@click.argument("reaction")
@click.argument("message_url")
@click.pass_context
def reaction(ctx, reaction: str, message_url: str):
"""
React with an emoji to a message.
"""
slack = slackapi_from_queries("qontract-cli", init_usergroups=False)
slack.add_reaction(reaction, message_url)


@set_command.command()
@click.argument("org_name")
@click.argument("cluster_name")
Expand Down