def handler(event, context): body = event['body'] params = parse_qs(body) channel_id = params['channel_id'][0] user_id = params['user_id'][0] if 'text' not in params: return cmd_help() res = token_table.get_item(Key={'user_id': user_id}) if 'Item' not in res: return please_install() encrypt_token = res['Item']['token'] blob_token = base64.b64decode(encrypt_token) kms_res = kms_client.decrypt(CiphertextBlob=blob_token) decrypted_token = kms_res['Plaintext'].decode('utf-8') input_text = params['text'][0].split() TargetLanguageCode = input_text[0] main_text = input_text[1] response = translate.translate_text(Text=main_text, SourceLanguageCode='auto', TargetLanguageCode=TargetLanguageCode) client = WebClient(token=decrypted_token) client.chat_postMessage(channel=channel_id, text=response['TranslatedText']) return { 'statusCode': HTTPStatus.OK, 'body': json.dumps({'text': 'Input: ' + input_text[1]}), 'headers': { 'Content-Type': 'application/json' } }
def send_message(): slack_info = SlackInfo.query.filter_by( team_name=request.get_json()["team"]).first() if slack_info is None: return jsonify({"type": "not_found", "text": "Team not found"}), 400 slack = WebClient(token=slack_info.bot_access_token) try: slack.chat_postMessage(channel=request.get_json()['channel'], text=request.get_json()['text']) except SlackApiError as error: if error.response.data["error"] == "channel_not_found": return jsonify({ "type": "not_found", "text": "Channel not found" }), 400 elif error.response.data["error"] == "not_in_channel": return jsonify({ "type": "not_in_channel", "text": "The bot is not a member of the channel" }), 400 return make_response("", 200)
class SlackClient: def __init__(self): self.client = WebClient(token=SLACK_BOT_TOKEN) def _raw_send_slack_message(self, channel: str, message: str) -> Union[Future, SlackResponse]: return self.client.chat_postMessage( channel=channel, text=message, ) def send_slack_message(self, channel: str, message: str) -> Union[Future, SlackResponse]: while True: try: return self._raw_send_slack_message(channel, message) except SlackApiError as e: if e.response["error"] == "ratelimited": delay = int(e.response.headers['Retry-After']) print(f"Rate limited. Retrying in {delay} seconds") time.sleep(delay) return self._raw_send_slack_message(channel, message) else: # other errors raise e
def sendMessage(msg, test): SLACK_BOT_TOKEN = os.environ['TAT_alerts_slack_bot_token'] slack_client = WebClient(SLACK_BOT_TOKEN) logging.debug("authorized slack client") # make the POST request through the python slack client channelname = "#tat-alerts" if test: channelname = channelname + '-test' # check if the request was a success try: slack_client.chat_postMessage( channel=channelname, text=msg ) except SlackApiError as e: logging.error('Request to Slack API Failed: {}.'.format(e.response.status_code)) logging.error(e.response)
def on_echo_command(): slack_info = SlackInfo.query.filter_by( team_id=request.values["team_id"]).first() slack = WebClient(slack_info.bot_access_token) pprint(request.values.to_dict()) try: slack.chat_postMessage( channel=request.values['channel_id'], text=f"{request.values['user_name']} said: {request.values['text']}" ) except SlackApiError: post(url=request.values["response_url"], json={ "response_type": "ephemeral", "text": "The bot *is not* a member of the channel" }) return make_response('', 200)
def execute(self, dag, task, execution_date, run_time, url): client = WebClient(token=self._slack_token) slack_msg = f""" :red_circle: {' '.join(self._mentions)} Task Failed. *Task*: {task} *Dag*: {dag} *Execution Time*: {execution_date} *Running For*: {run_time} secs *Log Url*: {url} """ response = client.chat_postMessage(link_names=1, channel=self._channel, text=slack_msg) print(response)
class SlackIntegration(Integration): api_token = models.CharField(_("API token"), max_length=100) channel = models.CharField(_("channel"), max_length=100) notify_issue_create = models.BooleanField(_("notify on issue creation"), default=True) notify_issue_modify = models.BooleanField(_("notify on issue modification"), default=True) notify_comment_create = models.BooleanField(_("notify when there is a new comment"), default=True) notify_sprint_start = models.BooleanField(_("notify when a sprint is started"), default=True) notify_sprint_stop = models.BooleanField(_("notify when when a sprint is stopped"), default=True) slack = None class Meta: verbose_name = _("slackintegration") verbose_name_plural = _("slackintegrations") def __str__(self): return self.channel def connect_signals(self): if self.notify_issue_create: signals.create.connect(self.on_issue_signal, weak=False, sender=Issue) if self.notify_issue_modify: signals.modify.connect(self.on_issue_signal, weak=False, sender=Issue) if self.notify_comment_create: signals.create.connect(self.on_comment_signal, weak=False, sender=Comment) if self.notify_sprint_start: signals.start.connect(self.on_sprint_signal, weak=False, sender=Sprint) if self.notify_sprint_stop: signals.stop.connect(self.on_sprint_signal, weak=False, sender=Sprint) def disconnect_signals(self): if not self.notify_issue_create: signals.create.disconnect(self.on_issue_signal, sender=Issue) if not self.notify_issue_modify: signals.modify.disconnect(self.on_issue_signal, sender=Issue) if not self.notify_comment_create: signals.create.disconnect(self.on_comment_signal, sender=Comment) if not self.notify_sprint_start: signals.start.disconnect(self.on_sprint_signal, sender=Sprint) if not self.notify_sprint_stop: signals.stop.disconnect(self.on_sprint_signal, sender=Sprint) def save(self, *args, **kwargs): if not self.pk: self.connect_signals() else: self.disconnect_signals() super(SlackIntegration, self).save(*args, **kwargs) def slack_on(self): if not self.slack: self.slack = WebClient(token=self.api_token) def on_issue_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].project != self.project: return self.slack_on() issue = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + issue.get_absolute_url() issue_title = issue.get_ticket_identifier() + ' ' + issue.title user_name = str(user) user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url if signal == signals.modify: text = '{} changed issue {}.'.format(user_name, issue_title) fields = [] for field in kwargs['changed_data']: old_str = kwargs['changed_data'][field] new_field = issue.__getattribute__(field) new_str = str(new_field) # Ducktyping for RelatedManager if hasattr(new_field, "add") and \ hasattr(new_field, "create") and \ hasattr(new_field, "remove") and \ hasattr(new_field, "clear") and \ hasattr(new_field, "set"): new_str = ", ".join([str(e) for e in new_field.all()]) fields.append({ 'title': Issue._meta.get_field(field).verbose_name.title(), 'value': '{} → {}'.format(old_str, new_str), 'short': True, }) if field == 'description': fields[-1]['short'] = False resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'Issue changed:', 'title': issue_title, 'title_link': title_link, 'author_name': user_name, 'author_link': user_link, 'author_icon': user_avatar, 'fields': fields, 'color': 'good', }] ) elif signal == signals.create: text = '{} created issue {}.'.format(user_name, issue_title) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'New Issue:', 'text': issue.description, 'title': issue_title, 'title_link': title_link, 'author_name': user_name, 'author_link': user_link, 'author_icon': user_avatar, 'color': 'good', }] ) def on_comment_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].issue.project != self.project: return self.slack_on() comment = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + comment.issue.get_absolute_url() issue_title = comment.issue.get_ticket_identifier() + ' ' + comment.issue.title user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url if signal == signals.create: text = '{} commented on "{}".'.format(str(user), issue_title) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'New comment:', 'text': comment.text, 'title': issue_title, 'title_link': title_link, 'author_name': str(user), 'author_link': user_link, 'author_icon': user_avatar, 'color': 'good', }] ) def on_sprint_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].project != self.project: return self.slack_on() sprint = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + reverse("backlog:backlog", kwargs={'project': self.project.name_short}) title = "sprint {}".format(sprint.seqnum) user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url action = "" text = "" if signal == signals.start: action = "started" text = '{} started {}.'.format(str(user), title) elif signal == signals.stop: action = "stopped" text = '{} stopped {}.'.format(str(user), title) title = title.capitalize() date_format = "%D" fields = [] fields.append({ 'title': _("Started"), 'value': sprint.startdate.strftime(date_format), 'short': True, }) if action == "stopped": fields.append({ 'title': _("Stopped"), 'value': sprint.enddate.strftime(date_format), 'short': True, }) if sprint.plandate: fields.append({ 'title': _("Planned end"), 'value': sprint.plandate.strftime(date_format), 'short': True, }) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'Sprint {}:'.format(action), 'text': '', 'title': title, 'title_link': title_link, 'author_name': str(user), 'author_link': user_link, 'author_icon': user_avatar, 'fields': fields, 'color': 'good', }] ) def user_has_write_permissions(self, user): return self.project.is_manager(user) def user_has_read_permissions(self, user): return self.project.user_has_read_permission(user)
class SlackDispatcher(Dispatcher): """Dispatch messages and cards to Slack.""" platform_name = "Slack" platform_slug = "slack" platform_color = "4A154B" # Slack Aubergine command_prefix = settings.PLUGINS_CONFIG["nautobot_chatops"][ "slack_slash_command_prefix"] """Prefix prepended to all commands, such as "/" or "!" in some clients.""" def __init__(self, *args, **kwargs): """Init a SlackDispatcher.""" super().__init__(*args, **kwargs) self.slack_client = WebClient( token=settings.PLUGINS_CONFIG["nautobot_chatops"] ["slack_api_token"]) @classmethod @BACKEND_ACTION_LOOKUP.time() def platform_lookup(cls, item_type, item_name): """Call out to the chat platform to look up, e.g., a specific user ID by name. Args: item_type (str): One of "organization", "channel", "user" item_name (str): Uniquely identifying name of the given item. Returns: (str, None) """ instance = cls(context=None) cursor = None if item_type == "organization": # pylint: disable=no-else-raise # The admin_teams_list API requires admin access and only works under Enterprise raise NotImplementedError elif item_type == "channel": while True: try: response = instance.slack_client.conversations_list( cursor=cursor, limit=20, exclude_archived=True) except SlackApiError as err: if err.response["error"] == "ratelimited": delay = int(err.response.headers["Retry-After"]) time.sleep(delay) continue raise err for channel in response["channels"]: if channel["name"] == item_name: return channel["id"] cursor = response["response_metadata"]["next_cursor"] if not cursor: break elif item_type == "user": while True: try: response = instance.slack_client.users_list(cursor=cursor, limit=20) except SlackApiError as err: if err.response["error"] == "ratelimited": delay = int(err.response.headers["Retry-After"]) time.sleep(delay) continue raise err for member in response["members"]: if member["name"] == item_name: return member["id"] cursor = response["response_metadata"]["next_cursor"] if not cursor: break return None # More complex APIs for presenting structured data - these typically build on the more basic functions below def command_response_header(self, command, subcommand, args, description="information", image_element=None): """Construct a consistently forwarded header including the command that was issued. Args: command (str): Primary command string subcommand (str): Secondary command string args (list): of tuples, either (arg_name, human_readable_value, literal_value) or (arg_name, literal_value) description (str): Short description of what information is contained in the response image_element (dict): As constructed by self.image_element() """ fields = [] for name, value, *_ in args: fields.append(self.markdown_element(self.bold(name))) fields.append(self.markdown_element(value)) command = f"{self.command_prefix}{command}" block = { "type": "section", "text": self.markdown_element( f"Hey {self.user_mention()}, here is that {description} you requested\n" f"Shortcut: `{command} {subcommand} {' '.join(arg[-1] for arg in args)}`" ), } # Add to block "accessory" key if image_element exists. Otherwise do not if image_element: block["accessory"] = image_element # Slack doesn't like it if we send an empty fields list, we have to omit it entirely if fields: block["fields"] = fields return [block] # Send various content to the user or channel @BACKEND_ACTION_MARKDOWN.time() def send_markdown(self, message, ephemeral=False): """Send a Markdown-formatted text message to the user/channel specified by the context.""" try: if ephemeral: self.slack_client.chat_postEphemeral( channel=self.context.get("channel_id"), user=self.context.get("user_id"), text=message, ) else: self.slack_client.chat_postMessage( channel=self.context.get("channel_id"), user=self.context.get("user_id"), text=message, ) except SlackClientError as slack_error: self.send_exception(slack_error) @BACKEND_ACTION_BLOCKS.time() def send_blocks(self, blocks, callback_id=None, ephemeral=False, modal=False, title="Your attention please!"): """Send a series of formatting blocks to the user/channel specified by the context. Slack distinguishes between simple inline interactive elements and modal dialogs. Modals can contain multiple inputs in a single dialog; more importantly for our purposes, certain inputs (such as textentry) can ONLY be used in modals and will be rejected if we try to use them inline. Args: blocks (list): List of block contents as constructed by other dispatcher functions callback_id (str): Callback ID string such as "command subcommand arg1 arg2". Required if `modal` is True. ephemeral (bool): Whether to send this as an ephemeral message (only visible to the targeted user) modal (bool): Whether to send this as a modal dialog rather than an inline block. title (str): Title to include on a modal dialog. """ logger.info("Sending blocks: %s", json.dumps(blocks, indent=2)) try: if modal: if not callback_id: self.send_error( "Tried to create a modal dialog without specifying a callback_id" ) return self.slack_client.views_open( trigger_id=self.context.get("trigger_id"), view={ "type": "modal", "title": self.text_element(title), "submit": self.text_element("Submit"), "blocks": blocks, # Embed the current channel information into to the modal as modals don't store this otherwise "private_metadata": json.dumps({ "channel_id": self.context.get("channel_id"), }), "callback_id": callback_id, }, ) elif ephemeral: self.slack_client.chat_postEphemeral( channel=self.context.get("channel_id"), user=self.context.get("user_id"), blocks=blocks, ) else: self.slack_client.chat_postMessage( channel=self.context.get("channel_id"), user=self.context.get("user_id"), blocks=blocks, ) except SlackClientError as slack_error: self.send_exception(slack_error) @BACKEND_ACTION_SNIPPET.time() def send_snippet(self, text): """Send a longer chunk of text as a file snippet.""" if self.context.get("channel_name") == "directmessage": channels = [self.context.get("user_id")] else: channels = [self.context.get("channel_id")] channels = ",".join(channels) logger.info("Sending snippet to %s: %s", channels, text) try: self.slack_client.files_upload(channels=channels, content=text) except SlackClientError as slack_error: self.send_exception(slack_error) def send_image(self, image_path): """Send an image as a file upload.""" if self.context.get("channel_name") == "directmessage": channels = [self.context.get("user_id")] else: channels = [self.context.get("channel_id")] channels = ",".join(channels) logger.info("Sending image %s to %s", image_path, channels) self.slack_client.files_upload(channels=channels, file=image_path) def send_warning(self, message): """Send a warning message to the user/channel specified by the context.""" self.send_markdown(f":warning: {self.bold(message)} :warning:", ephemeral=True) def send_error(self, message): """Send an error message to the user/channel specified by the context.""" self.send_markdown(f":warning: {self.bold(message)} :warning:", ephemeral=True) def send_busy_indicator(self): """Send a "typing" indicator to show that work is in progress.""" # Currently the Slack Events API does not support the "user_typing" event. # We're trying not to use the legacy Slack RTM API as it's deprecated. # So for now, we do nothing. pass def send_exception(self, exception): """Try to report an exception to the user.""" self.slack_client.chat_postEphemeral( channel=self.context.get("channel_id"), user=self.context.get("user_id"), text= f"Sorry @{self.context.get('user_name')}, an error occurred :sob:\n```{exception}```", ) def delete_message(self, response_url): """Delete a message that was previously sent.""" WebhookClient(response_url).send_dict({"delete_original": "true"}) # Prompt the user for various basic inputs def prompt_for_text(self, action_id, help_text, label, title="Your attention please!"): """Prompt the user to enter freeform text into a field. Args: action_id (str): Identifier string to attach to the "submit" action. help_text (str): Markdown string to display as help text. label (str): Label text to display adjacent to the text field. title (str): Title to include on the modal dialog. """ textentry = { "type": "input", "block_id": action_id, "label": self.text_element(label), "element": { "type": "plain_text_input", "action_id": action_id, "placeholder": self.text_element(label) }, } blocks = [self.markdown_block(help_text), textentry] # In Slack, a textentry element can ONLY be sent in a modal dialog return self.send_blocks(blocks, callback_id=action_id, ephemeral=True, modal=True, title=title) def prompt_from_menu(self, action_id, help_text, choices, default=(None, None), confirm=False): """Prompt the user for a selection from a menu. Args: action_id (str): Identifier string to attach to the "submit" action. help_text (str): Markdown string to display as help text. choices (list): List of (display, value) tuples default (tuple): Default (display, valud) to pre-select. confirm (bool): If True, prompt the user to confirm their selection (if the platform supports this) """ # TODO: Slack limits an option list to no more than 100 items. How can we work around this? if len(choices) > 100: self.send_warning( "More than 100 options are available. Slack limits us to the first 100." ) choices = choices[:100] menu = self.select_element(action_id, choices, default=default, confirm=confirm) cancel_button = { "type": "button", "text": self.text_element("Cancel"), "action_id": "action", "value": "cancel", } blocks = [ self.markdown_block(help_text), self.actions_block(action_id, [menu, cancel_button]) ] return self.send_blocks(blocks, ephemeral=True) # Construct content piecemeal, mostly for use with send_blocks() def multi_input_dialog(self, command, sub_command, dialog_title, dialog_list): """Provide several input fields on a single dialog. Args: command (str): The top level command in use. (ex. net) sub_command (str): The command being invoked (ex. add-vlan) dialog_title (str): Title of the dialog box dialog_list (list): List of dictionaries containing the dialog parameters. See Example below. Example: For a selection menu:: { type: "select", label: "label", choices: [("display 1", "value1"), ("display 2", "value 2")], default: ("display 1", "value1"), confirm: False } For a text dialog:: { type: "text", label: "text displayed next to field" default: "default-value" } Dictionary Fields - type: The type of object to create. Currently supported values: select, text - label: A text descriptor that will be placed next to the field - choices: A list of tuples which populates the choices in a dropdown selector - default: (optional) Default choice of a select menu or initial value to put in a text field. - confirm: (optional) If set to True, it will display a "Are you sure?" dialog upon submit. """ blocks = [] callback_id = f"{command} {sub_command}" for i, dialog in enumerate(dialog_list): action_id = f"param_{i}" if dialog["type"] == "select": menu = self.select_element(action_id, dialog["choices"], dialog.get("default", (None, None)), dialog.get("confirm", False)) blocks.append( self._input_block(action_id, dialog["label"], menu)) if dialog["type"] == "text": textentry = { "type": "plain_text_input", "action_id": action_id, "placeholder": self.text_element(dialog["label"]), "initial_value": dialog.get("default", "") or "", } blocks.append( self._input_block(action_id, dialog["label"], textentry)) return self.send_blocks(blocks, callback_id=callback_id, modal=True, ephemeral=False, title=dialog_title) def user_mention(self): """Markup for a mention of the username/userid specified in our context.""" return f"<@{self.context['user_id']}>" def bold(self, text): """Mark text as bold.""" return f"*{text}*" def actions_block(self, block_id, actions): """Construct a block consisting of a set of action elements.""" return {"type": "actions", "block_id": block_id, "elements": actions} def _input_block(self, block_id, label, element): """Construct a block consisting of Input elements.""" text_obj = self.text_element(label) return { "type": "input", "block_id": block_id, "label": text_obj, "element": element } def markdown_block(self, text): """Construct a simple Markdown-formatted text block.""" return {"type": "section", "text": self.markdown_element(text)} def image_element(self, url, alt_text=""): """Construct an image element that can be embedded in a block.""" return {"type": "image", "image_url": url, "alt_text": alt_text} def markdown_element(self, text): """Construct a basic Markdown-formatted text element.""" return {"type": "mrkdwn", "text": text} def select_element(self, action_id, choices, default=(None, None), confirm=False): """Construct a basic selection menu with the given choices. .. note:: Slack's select menu supports option groups, but others such as Adaptive Cards do not; we have to stick to the lowest common denominator, which means no groups. TODO: maybe refactor this API so that Slack can use groups and other dispatchers just ignore the groups? Args: action_id (str): Identifying string to associate with this element choices (list): List of (display, value) tuples default (tuple: Default (display, value) to preselect confirm (bool): If true (and the platform supports it), prompt the user to confirm their selection """ data = { "type": "static_select", "action_id": action_id, "placeholder": self.text_element("Select an option"), "options": [{ "text": self.text_element(choice), "value": value } for choice, value in choices], } default_text, default_value = default if default_text and default_value: data["initial_option"] = { "text": self.text_element(default_text), "value": default_value } if confirm: data["confirm"] = { "title": self.text_element("Are you sure?"), "text": self.markdown_element( "Are you *really* sure you want to do this?"), "confirm": self.text_element("Yes"), "deny": self.text_element("No"), } return data def text_element(self, text): """Construct a basic plaintext element.""" return {"type": "plain_text", "text": text}