Source code for slack_functions

"""
slack functions

These are shared slack functions that are required in multiple modules.
"""
import os
import logging
import traceback

import settings

# @modified 20200701 - Task #3612: Upgrade to slack v2
#                      Task #3608: Update Skyline to Python 3.8.3 and deps
#                      Task #3556: Update deps
# from slackclient import SlackClient
# slackclient v2 has a version function, < v2 does not
try:
    from slack import version as slackVersion
    slack_version = slackVersion.__version__
except:
    slack_version = '1.3'
if slack_version == '1.3':
    from slackclient import SlackClient
else:
    from slack import WebClient

token = settings.SLACK_OPTS['bot_user_oauth_access_token']
try:
    icon_emoji = settings.SLACK_OPTS['icon_emoji']
except:
    icon_emoji = ':chart_with_upwards_trend:'

# @added 20230605 - Feature #4932: mute_alerts_on
default_channel = 'general'
try:
    default_channel = settings.SLACK_OPTS['default_channel']
except:
    default_channel = 'general'

# @modified 20240729 - Feature #5418: slack_post_message - image_file parameter
# Added image_file parameter
[docs] def slack_post_message(current_skyline_app, channel, thread_ts, message, image_file=None): """ Post a message to a slack channel or thread. :param current_skyline_app: the skyline app using this function :param channel: the slack channel :param thread_ts: the slack thread timestamp :param message: message :param image: the path and filename of an attach to attach to the slack post :type current_skyline_app: str :type channel: str :type thread_ts: str or None :type message: str :return: slack response dict :rtype: dict """ current_skyline_app_logger = str(current_skyline_app) + 'Log' current_logger = logging.getLogger(current_skyline_app_logger) # @added 20200826 - Bug #3710: Gracefully handle slack failures slack_post = {'ok': False} # @modified 20240729 - Feature #5418: slack_post_message - image parameter # Added image_file parameter use_slack_file_upload = False if image_file: if os.path.isfile(image_file): use_slack_file_upload = True if use_slack_file_upload: try: uploaded = slack_file_upload(current_skyline_app, channel, thread_ts, message, image_file=image_file) if uploaded: slack_post['ok'] = True slack_post['file_uploaded'] = True return slack_post except Exception as err: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_post_message :: falied to connect slack, err: %s' % err) slack_post['file_uploaded'] = False return slack_post try: # @modified 20200701 - Task #3612: Upgrade to slack v2 # Task #3608: Update Skyline to Python 3.8.3 and deps # Task #3556: Update deps if slack_version == '1.3': sc = SlackClient(token) else: sc = WebClient(token, timeout=10) except: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_post_message :: falied to connect slack') # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_post if thread_ts: if thread_ts == 'None': thread_ts = None # @added 20230605 - Feature #4932: mute_alerts_on if not channel: channel = default_channel # slack_post = None # In terms of the generated Slack URLS for threads the # timestamps have no dots e.g.: # https://<an_org>.slack.com/archives/<a_channel>/p1543994173000700 # However in terms of the sc.api_call the thread_ts # needs the format declared in the dict response e.g. # u'ts': u'1543994173.000700'}]} with the dot so in this # case '1543994173.000700' if thread_ts: try: # @modified 20200701 - Task #3612: Upgrade to slack v2 if slack_version == '1.3': slack_post = sc.api_call( 'chat.postMessage', channel=channel, icon_emoji=icon_emoji, text=message, thread_ts=thread_ts ) else: slack_post = sc.chat_postMessage( channel=channel, icon_emoji=icon_emoji, text=message, thread_ts=thread_ts ) except Exception as err: # @added 20220422 - Bug #4448: Handle missing slack response # Handle slack SSL errors which occur more than one would expect if 'CERTIFICATE_VERIFY_FAILED' in str(err): slack_post = {'ok': False, 'slack_ssl_error': True} fail_msg = 'warning :: slack_post_message :: failed to post message to thread (slack SSL issue) - %s - %s - %s' % ( thread_ts, message, err) current_logger.warning('%s' % fail_msg) else: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_post_message :: failed to post message channel: %s, to thread %s - %s - %s' % ( str(channel), str(thread_ts), message, err)) # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_post else: try: # @modified 20200701 - Task #3612: Upgrade to slack v2 if slack_version == '1.3': slack_post = sc.api_call( 'chat.postMessage', channel=channel, icon_emoji=icon_emoji, text=message ) else: slack_post = sc.chat_postMessage( channel=channel, icon_emoji=icon_emoji, text=message ) except Exception as err: # @added 20220422 - Bug #4448: Handle missing slack response # Handle slack SSL errors which occur more than one would expect if 'CERTIFICATE_VERIFY_FAILED' in str(err): slack_post = {'ok': False, 'slack_ssl_error': True} fail_msg = 'warning :: slack_post_message :: failed to post message to thread (slack SSL issue) - %s - %s - %s' % ( thread_ts, message, err) current_logger.warning('%s' % fail_msg) else: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_post_message :: failed to post message channel: %s, to thread %s - %s - %s' % ( str(channel), str(thread_ts), message, err)) # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_post if slack_post['ok']: current_logger.info( 'slack_post_message :: posted message to channel %s, thread %s - %s' % ( channel, str(thread_ts), message)) else: current_logger.error( 'error :: slack_post_message :: failed to post message to channel %s, thread %s - %s' % ( channel, str(thread_ts), message)) current_logger.error( 'error :: slack_post_message :: slack response dict follows') try: current_logger.error(str(slack_post)) except: current_logger.error('error :: slack_post_message :: no slack response dict found') # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_post return slack_post
[docs] def slack_post_reaction(current_skyline_app, channel, thread_ts, emoji): """ Post a message to a slack channel or thread. :param current_skyline_app: the skyline app using this function :param channel: the slack channel :param thread_ts: the slack thread timestamp :param emoji: emoji e.g. thumbsup :type current_skyline_app: str :type channel: str :type thread_ts: str :type emoji: str :return: slack response dict :rtype: dict """ current_skyline_app_logger = str(current_skyline_app) + 'Log' current_logger = logging.getLogger(current_skyline_app_logger) # @added 20200826 - Bug #3710: Gracefully handle slack failures slack_response = {'ok': False} try: # @modified 20200701 - Task #3612: Upgrade to slack v2 # Task #3608: Update Skyline to Python 3.8.3 and deps # Task #3556: Update deps # sc = SlackClient(token) if slack_version == '1.3': sc = SlackClient(token) else: sc = WebClient(token, timeout=10) except: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_post_message :: failed to connect slack') # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_response # slack_response = None # @added 20220328 - Bug #4448: Handle missing slack response # Handle slack SSL errors which occur more than one would expect slack_ssl_error = False # @added 20230605 - Feature #4932: mute_alerts_on if not channel: channel = default_channel try: # @modified 20200701 - Task #3612: Upgrade to slack v2 # Task #3608: Update Skyline to Python 3.8.3 and deps # Task #3556: Update deps if slack_version == '1.3': slack_response = sc.api_call( 'reactions.add', channel=channel, name=emoji, timestamp=thread_ts ) else: slack_response = sc.reactions_add( channel=channel, name=emoji, timestamp=thread_ts ) except Exception as err: # @added 20200826 - Bug #3710: Gracefully handle slack failures # Handle already_reacted if 'already_reacted' in str(err): current_logger.info( 'slack_post_reaction :: post reaction to thread %s - %s - (already_reacted)' % ( thread_ts, emoji)) slack_response['ok'] = True elif 'CERTIFICATE_VERIFY_FAILED' in str(err): slack_ssl_error = True # @added 20220422 - Bug #4448: Handle missing slack response # Handle slack SSL errors which occur more than one would expect slack_response = {'ok': False, 'slack_ssl_error': True} fail_msg = 'warning :: create_features_profile :: failed to slack_post_message - %s' % err current_logger.warning('%s' % fail_msg) else: current_logger.warning(traceback.format_exc()) current_logger.warning( 'warning :: slack_post_reaction :: failed to post reaction to thread %s - %s - %s' % ( thread_ts, emoji, err)) # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_response if not slack_response['ok'] and not slack_ssl_error: # @modified 20220214 - Bug #4448: Handle missing slack response # if str(slack_response['error']) == 'already_reacted': slack_response_error = None try: slack_response_error = slack_response['error'] except KeyError: slack_response_error = slack_response['error'] fail_msg = 'error :: create_features_profile :: no slack response' current_logger.error('%s' % fail_msg) if slack_response_error == 'already_reacted': current_logger.info( 'slack_post_reaction :: already_reacted to channel %s, thread %s, ok' % ( channel, str(thread_ts))) else: current_logger.error( 'error :: slack_post_reaction :: failed to post reaction to channel %s, thread %s - %s' % ( channel, str(thread_ts), emoji)) current_logger.error( 'error :: slack_post_reaction :: slack response dict follows') try: current_logger.error(str(slack_response)) except: current_logger.error('error :: slack_post_reaction :: no slack response dict found') # @modified 20200826 - Bug #3710: Gracefully handle slack failures # return False return slack_response return slack_response
# @added 20240729 - Feature #5418: slack_post_message - image_file parameter
[docs] def slack_file_upload(current_skyline_app, channel, thread_ts, message, image_file=None): """ Post a message with an image to a slack channel or thread. :param current_skyline_app: the skyline app using this function :param channel: the slack channel :param thread_ts: the slack thread timestamp :param message: message :param image_file: the path and filename of an attach to attach to the slack post :type current_skyline_app: str :type channel: str :type thread_ts: str or None :type message: str :return: slack response dict :rtype: dict """ current_skyline_app_logger = str(current_skyline_app) + 'Log' current_logger = logging.getLogger(current_skyline_app_logger) file_uploaded = False try: slack_thread_updates = settings.SLACK_OPTS['thread_updates'] except: slack_thread_updates = False if not settings.SLACK_ENABLED: slack_thread_updates = False slack_file_upload = False slack_thread_ts = 0 try: sc = WebClient(token, timeout=10) except: current_logger.error(traceback.format_exc()) current_logger.error( 'error :: slack_file_upload :: falied to connect slack') return file_uploaded if thread_ts: if thread_ts == 'None': thread_ts = None if not channel: channel = default_channel try: # slack does not allow embedded images, nor links behind authentication # or color text, so we have jump through all the API hoops to end up # having to upload an image with a very basic message. if os.path.isfile(image_file): filename = os.path.basename(image_file) slack_file_upload = sc.files_upload( filename=filename, channels=channel, initial_comment=message, file=open(image_file, 'rb')) if not slack_file_upload['ok']: current_logger.error('error :: slack_file_upload :: failed to send slack message') else: current_logger.info('slack_file_upload :: sent slack message') file_uploaded = True if slack_thread_updates: # The sc.api_call 'files.upload' response which generates # slack_file_upload has a different structure depending # on whether a channel is private or public. That also # goes for free or hosted slack too. slack_group = None slack_group_list = None # This is basically the channel id of your channel, the # name could be used so that if in future it is used or # displayed in a UI # This block only works for free slack workspace private # channels. Although this should be handled in the # SLACK_OPTS as slack_account_type: 'free|hosted' and # default_channel_type = 'private|public', it is going # to be handled in the code for the time being so as not # to inconvience users to update their settings.py for # v.1.2.17 # TODO next release with settings change add # these. slack_group = None slack_group_trace_groups = None slack_group_trace_channels = None try: slack_group = slack_file_upload['file']['groups'][0] current_logger.info('slack_file_upload :: slack group has been set from \'groups\' as %s' % ( str(slack_group))) slack_group_list = slack_file_upload['file']['shares']['private'][slack_group] slack_thread_ts = slack_group_list[0]['ts'].encode('utf-8') current_logger.info('slack_file_upload :: slack group is %s and the slack_thread_ts is %s' % ( str(slack_group), str(slack_thread_ts))) except: slack_group_trace_groups = traceback.format_exc() current_logger.info('slack_file_upload :: failed to determine slack_group using groups') if not slack_group: # Try by channel try: slack_group = slack_file_upload['file']['channels'][0] current_logger.info('slack_file_upload :: slack group has been set from \'channels\' as %s' % ( str(slack_group))) except: slack_group_trace_channels = traceback.format_exc() current_logger.info('slack_file_upload :: failed to determine slack_group using channels') current_logger.error('error :: slack_file_upload :: failed to determine slack_group using groups or channels') current_logger.error('error :: slack_file_upload :: traceback from slack_group_trace_groups follows:') current_logger.error(str(slack_group_trace_groups)) current_logger.error('error :: slack_file_upload :: traceback from slack_group_trace_channels follows:') current_logger.error(str(slack_group_trace_channels)) current_logger.error('error :: slack_file_upload :: faied to determine slack_thread_ts') slack_group_list = None if slack_group: slack_group_list_trace_private = None slack_group_list_trace_public = None # Try private channel try: slack_group_list = slack_file_upload['file']['shares']['private'][slack_group] current_logger.info('slack_file_upload :: slack_group_list determined from private channel and slack_group %s' % ( str(slack_group))) except: slack_group_list_trace_private = traceback.format_exc() current_logger.info('slack_file_upload :: failed to determine slack_group_list using private channel') if not slack_group_list: # Try public channel try: slack_group_list = slack_file_upload['file']['shares']['public'][slack_group] current_logger.info('slack_file_upload :: slack_group_list determined from public channel and slack_group %s' % ( str(slack_group))) except: slack_group_list_trace_public = traceback.format_exc() current_logger.info('slack_file_upload :: failed to determine slack_group_list using public channel') current_logger.info('slack_file_upload :: failed to determine slack_group_list using private or public channel') current_logger.error('error :: slack_file_upload :: traceback from slack_group_list_trace_private follows:') current_logger.error(str(slack_group_list_trace_private)) current_logger.error('error :: slack_file_upload :: traceback from slack_group_list_trace_public follows:') current_logger.error(str(slack_group_list_trace_public)) current_logger.error('error :: slack_file_upload :: faied to determine slack_thread_ts') if slack_group_list: try: slack_thread_ts = slack_group_list[0]['ts'] current_logger.info('slack_file_upload :: slack group is %s and the slack_thread_ts is %s' % ( str(slack_group), str(slack_thread_ts))) return file_uploaded except: current_logger.error(traceback.format_exc()) current_logger.info('slack_file_upload :: failed to determine slack_thread_ts') return file_uploaded else: send_text = message + ' :: error :: there was no image to upload' send_message = sc.api_call( 'chat.postMessage', channel=channel, icon_emoji=icon_emoji, text=send_text) if not send_message['ok']: current_logger.error('error :: slack_file_upload :: failed to send slack message') return False else: current_logger.info('slack_file_upload :: sent slack message') return True except: current_logger.info(traceback.format_exc()) current_logger.error('error :: slack_file_upload :: could not upload file') return False