def find_or_create_channel(self, input_channel_name, slack_is_private, res_client, incident_id, task_id): """ If channel_name is NOT specified in the function input, method will perform a lookup for associated channel_name in Slack Conversations Datatable. If there is an Incident or Task associated slack_channel, method will use found channel name for either searching for an existing Slack channel in Slack Workspace (api call) or create a new Slack channel (api call). If channel_name is specified in the function input, method will search for an existing Slack channel in Slack Workspace (api call) or create a new Slack channel (api call) with the specified channel_name. If channel_name is specified and there is also the associated channel found, method will ignore the associated one and post to the input one. :param input_channel_name: :param slack_is_private: :param res_client: :param incident_id: :param task_id: :return: slack_channel_name, True if res_associated_channel_name exists """ # Pick the right channel to post in slack_channel_name, res_associated_channel_name = self._find_the_proper_channel_name( input_channel_name, res_client, incident_id, task_id) # find the channel in Slack Workspace self.find_channel_by_name(slack_channel_name) # validation for channels existing in Slack Workspace if self.get_channel(): self.add_warning(u"Channel #{} was found in your Workspace".format(slack_channel_name)) # channel_name can be unicode # validate if your fun input param 'slack_is_private' matches channel's type, if not stop the workflow if slack_is_private and not self.is_channel_private(): raise IntegrationError(u"You've indicated the channel you are posting to should be private. " u"The existing channel #{} you are posting to is a public channel. " u"To post to this channel change the input parameter " u"'slack_is_channel_private' to 'No'.".format(slack_channel_name)) elif slack_is_private is False and self.is_channel_private(): raise IntegrationError(u"You've indicated the channel you are posting to should be public. " u"The existing channel #{} you are posting to is a private channel. " u"To post to this channel change the input parameter " u"'slack_is_channel_private' to 'Yes'.".format(slack_channel_name)) elif self.is_channel_archived(): raise IntegrationError(u"Channel {} is archived".format(slack_channel_name)) # create a new channel else: # validate slack_is_private is defined, check for None only, False is ok if slack_is_private is None: raise ValueError("Required field 'slack_is_private' is missing or empty") self.slack_create_channel(slack_channel_name, slack_is_private) # rewrite slack_channel_name just in case Slack validation modifies the submitted channel name slack_channel_name = self.get_channel_name() self.add_warning(u"Channel #{} was created in your Workspace".format(slack_channel_name)) return slack_channel_name, res_associated_channel_name is not None
def _slack_find_channels(self, cursor=None): """ Method returns a list of all public or private channels in a workspace. Using Conversations API to access anything channel-like (private, public, direct, etc) Supports pagination. Cursor-based pagination will make it easier to incrementally collect information. To begin pagination, specify a limit value under 1000. Slack recommends no more than 200 results at a time. Paginate only until channel is found. :return: list of channels """ has_more_results = True while has_more_results: results = self.slack_client.api_call( "conversations.list", exclude_archived=False, # we need to load archived channels types="public_channel,private_channel", limit=SLACK_LOAD_CHANNELS_LIMIT, cursor=cursor ) LOG.debug(results) if not results.get("ok"): raise IntegrationError("Slack error response: " + results.get("error", "")) has_more_results, cursor = self._get_next_cursor_for_next_page(results) for ch in results.get("channels"): # yield the first page, paginate only until channel is found! yield ch
def _get_channel_thread_message_history(self, msg_ts, cursor=None): """ Method returns the entire thread (the parent message plus all the thread replies). "conversations.replies" function supports pagination. Cursor-based pagination will make it easier to incrementally collect information. To begin pagination, specify a limit value under 1000. Slack recommends no more than 200 results at a time. :return: list of entire thread (parent plus reply) messages """ thread_messages_list = [] has_more_results = True while has_more_results: results = self.slack_client.api_call( "conversations.replies", channel=self.get_channel_id(), ts=msg_ts, limit=SLACK_HISTORY_REPLY_MESSAGE_LIMIT, cursor=cursor ) LOG.debug(results) if not results.get("ok"): raise IntegrationError("Slack error response: " + results.get("error", "")) has_more_results, cursor = self._get_next_cursor_for_next_page(results) thread_messages_list.extend(results.get("messages")) return thread_messages_list
def _get_channel_parent_message_history(self, cursor=None): """ Method returns only parent messages from a conversation. "conversations.history" function supports pagination. Need to call "conversations.history" method with no latest or oldest arguments, and then continue paging using the cursor. Cursor-based pagination will make it easier to incrementally collect information. To begin pagination, specify a limit value under 1000. Slack recommends no more than 200 results at a time. :return: list of parent messages """ parent_messages_list = [] has_more_results = True while has_more_results: results = self.slack_client.api_call( "conversations.history", channel=self.get_channel_id(), limit=SLACK_HISTORY_MESSAGE_LIMIT, cursor=cursor ) LOG.debug(results) if not results.get("ok"): raise IntegrationError("Slack error response: " + results.get("error", "")) has_more_results, cursor = self._get_next_cursor_for_next_page(results) parent_messages_list.extend(results.get("messages")) return parent_messages_list
def find_user_ids_based_on_email(self, slack_participant_emails): """ Find user ids based on their emails. :param slack_participant_emails: :return: user_id_list """ # Find user ids based on their emails user_id_list = [] emails = [ email.strip() for email in slack_participant_emails.split(",") if email.strip() ] # making sure to exclude ' ' or '' for email in emails: results_user_id = self.lookup_user_by_email(email) if results_user_id.get("ok") and results_user_id.get("user"): user_id_list.append(results_user_id.get("user").get("id")) elif not results_user_id.get("ok") and results_user_id.get( "error", "") == "users_not_found": self.add_warning( "User {} is not a member of your workspace".format(email)) else: raise IntegrationError("Invite users failed: " + json.dumps(results_user_id)) return user_id_list
def build_payload(ordered_data_dict): """ Build the payload string based on the different types of data created. :param ordered_data_dict: :return: payload string """ payload = "" for key, value in ordered_data_dict.items(): input_type = value.get("type") input_data = value.get("data") if input_type == 'string' and input_data: if payload: payload += "\n" matches = re.findall( r"u'(.*?)'", input_data ) # extract data from u'[u\\'Malware\\', u\\'Lost PC / laptop / tablet\\']' if matches: data = ", ".join(matches) payload += u'*{}*: {}'.format(key, data) else: payload += u'*{}*: {}'.format(key, input_data) elif input_type == 'richtext' and input_data: cleaned_data = clean_html(input_data) if cleaned_data: if payload: payload += "\n" payload += u'*{}*: {}'.format(key, cleaned_data) elif input_type == 'datetime' and input_data: if payload: payload += "\n" # Slack expects epoch date in seconds, using future import for division and using operator '//' to yield int in Py2 and Py3 payload += '*{}*: `<!date^{}^{{date_num}} {{time_secs}}|{}>`'.format( key, input_data // 1000, readable_datetime(input_data, True)) elif input_type == 'boolean' and input_data: if payload: payload += "\n" payload += '*{}*: {}'.format( key, build_boolean(input_data, true_value='Yes', false_value='No')) elif input_data: raise IntegrationError("Invalid type: " + input_type) return payload
def slack_post_message(self, resoptions, slack_text, slack_as_user, slack_username, slack_markdown, def_username): """ Process the slack post :param resoptions: app.config resilient section :param slack_text: json structure for how to structure the payload to slack See slack API for the use of these variables :param slack_as_user: :param slack_username: :param slack_markdown: :param def_username - name to use for who posted the message :return: JSON result """ attachment_json = None payload = None try: attachment_json = convert_slack_details_to_payload( slack_text, resoptions) # If slack_text can be converted to a python dictionary with json.loads the payload will be posted in Slack as # a Slack attachment. # If slack_text isn't in JSON format and json.loads returns an error the workflow does not terminate, # message is posted in Slack as a regular text and the message describing this action is saved to log. except ValueError as json_error: LOG.debug( "Warning - Cannot convert payload to JSON, posting to Slack as a regular text - " "see JSON message '{}'. %s", json_error) payload = slack_text results = self.slack_client.api_call( "chat.postMessage", channel=self.get_channel_id(), as_user=slack_as_user, username=slack_username if slack_username else def_username, # TODO Username to be deprecated! Slack apps and their bot users should not use the username field when authoring a message. The username is part of your app's configuration and will not always be settable at runtime. parse= "none", # Slack will not perform any processing on the message, it will keep all markup formatting '<' link_names= 1, # Slack will linkify URLs, channel names (starting with a '#') and usernames (starting with an '@') mrkdown=slack_markdown, attachments=attachment_json, text=payload) LOG.debug(results) if results.get("ok"): self.add_warning("Message added to Slack") return results else: raise IntegrationError("Message add failed: " + json.dumps(results))
def _get_user_info(self, user_id): """ This method returns information about a member of a workspace. :param user_id: :return: user """ results = self.slack_client.api_call("users.info", user=user_id) LOG.debug(results) if results.get("ok"): return results.get("user") else: raise IntegrationError("Slack error response: " + results.get("error", ""))
def get_permalink(self, thread_id): """ Retrieve a permalink URL for a specific extant message :param thread_id: A message's ts value, uniquely identifying it within a channel :return: permalink """ results = self.slack_client.api_call("chat.getPermalink", channel=self.get_channel_id(), message_ts=thread_id) LOG.debug(results) if results.get("ok"): return results.get("permalink") else: raise IntegrationError("Slack error response: " + results.get("error", ""))
def _find_the_proper_channel_name(self, input_channel_name, res_client, incident_id, task_id): """ Method will decided what channel name to use based on different params. :param input_channel_name: :param res_client: :param incident_id: :param task_id: :return: slack_channel_name, res_associated_channel_name """ # If user doesn't specify a channel name, use the incident/task associated channel (the default channel). res_associated_channel_name = slack_channel_name_datatable_lookup( res_client, incident_id, task_id) LOG.debug("slack_channel name associated with Incident or Task: %s", res_associated_channel_name) # Channel name validation - Channel name needs to be defined if input_channel_name is None and res_associated_channel_name is None: raise IntegrationError( "There is no slack_channel name associated with Incident or Task available " "to post messages in") # Pick the right channel to post in slack_channel_name = None if input_channel_name is None and res_associated_channel_name: # if there wasn't channel specified in the activity prompt, # post to the_channel associated with the Incident or Task slack_channel_name = res_associated_channel_name elif input_channel_name: # if there was channel specified in the activity prompt, # post to to this one and ignore the associated one slack_channel_name = input_channel_name if res_associated_channel_name: # If the associated channel exists yield a StatusMessage self.add_warning( u"This Incident or Task has an association with Slack channel #{}, " u"your message was posted in a different channel #{}". format(res_associated_channel_name, input_channel_name)) return slack_channel_name, res_associated_channel_name
def invite_users_to_channel(self, user_id_list): """ Method invites 1-30 users to a public or private channel. :param user_id_list: A comma separated list of user IDs. Up to 30 users may be listed. :return: JSON result """ users_id = ",".join(user_id_list) results = self.slack_client.api_call( "conversations.invite", channel=self.get_channel_id(), users=users_id ) LOG.debug(results) if results.get("ok"): self.add_warning(u"Users invited to channel #{}".format(self.get_channel_name())) elif not results.get("ok") and results.get("error") == "already_in_channel": self.add_warning(u"Invited user is already in #{} channel".format(self.get_channel_name())) else: raise IntegrationError("Invite users failed: " + json.dumps(results))
def slack_create_channel(self, slack_channel_name, is_private): """ Method creates a public or private channel and updates the channel instance variable. Using Conversations API to access anything channel-like (private, public, direct, etc). Channel names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 21 characters or less. Slack validates the submitted channel name and modifies it to meet the above criteria. Since the channel name can get modified use channel_id instead. :param slack_channel_name: Name of the public or private channel to create :param is_private: Create a private channel instead of a public one :return: """ results = self.slack_client.api_call("conversations.create", name=slack_channel_name, is_private=is_private) LOG.debug(results) if results.get("ok"): self.channel = results.get("channel") else: raise IntegrationError("Slack error response: " + results.get("error", ""))
def slack_post_attachment(self, attachment_content, attachment_data, slack_text): """ Function uploads file to your slack_channel. :param attachment_content: :param attachment_data: :param slack_text :return: JSON result """ attachment = attachment_data.get("attachment") if attachment: file_name = attachment.get("name") file_type = attachment.get("content_type") incident_id = attachment.get("inc_id") artifact_type = attachment.get("type") else: file_name = attachment_data.get("name") file_type = attachment_data.get("content_type") incident_id = attachment_data.get("inc_id") artifact_type = attachment_data.get("type") results = self.slack_client.api_call( "files.upload", channels=self.get_channel_id(), file=attachment_content, filename=file_name, filetype=file_type, title=u"Incident ID {} {} Attachment: {}".format( incident_id, artifact_type, file_name), initial_comment=slack_text) LOG.debug(results) if results.get("ok"): self.add_warning("Attachment uploaded to Slack") return results else: raise IntegrationError("File upload failed: " + json.dumps(results))
def _get_next_cursor_for_next_page(results): """ Cursor-based pagination will make it easier to incrementally collect information. :param results :return: has_more_results, cursor """ has_more_results = True cursor = None if results.get("ok"): response_metadata = results.get("response_metadata") if response_metadata: # more pages cursor = response_metadata.get("next_cursor") if not cursor: # we've reached the last page has_more_results = False else: # we've reached the last page has_more_results = False return has_more_results, cursor else: raise IntegrationError("Slack error response: " + results.get("error", ""))