class SlackBot: def __init__(self, token): self.client = WebClient(token=token) self.channels_dict = dict() try: for response in self.client.conversations_list(): self.channels_dict.update({ channel["name"]: channel["id"] for channel in response["channels"] }) except SlackApiError as e: print(f"Error: {e}") def check_channel(self, channel: str): return channel in self.channels_dict def post_in_channel(self, channel_name: str, message: str, articles: [Article]): if articles: marked_articles = '>- ' + '\n>- '.join( [f'<{art.url}|{art.header}>' for art in articles]) try: self.client.conversations_join( channel=self.channels_dict[channel_name]) self.client.chat_postMessage( channel=self.channels_dict[channel_name], text=f"{message}", blocks=[{ "type": "section", "text": { "type": "mrkdwn", "text": f"{message}" } }, { "type": "section", "text": { "type": "mrkdwn", "text": f"{marked_articles}" } }]) except SlackApiError as e: print(f"Error: {e}")
def join_channel(channel): """ If the app gets the 'not_in_channel' error when accessing a public channel, call this method :param channel: The channel to join :returns: Response object (Dictionary) """ if not settings.SLACK_TOKEN: return {'ok': False, 'error': 'config_error'} client = WebClient(token=settings.SLACK_TOKEN) try: response = client.conversations_join(channel=channel) assert response['ok'] is True return {'ok': response['ok']} except SlackApiError as e: assert e.response['ok'] is False return e.response
class SlackMonitor(Monitor): """ Create a monitoring service that alerts on Task failures / completion in a Slack channel """ def __init__(self, slack_api_token, channel, message_prefix=None, filters=None): # type: (str, str, Optional[str], Optional[List[Callable[[Task], bool]]]) -> () """ Create a Slack Monitoring object. It will alert on any Task/Experiment that failed or completed :param slack_api_token: Slack bot API Token. Token should start with "xoxb-" :param channel: Name of the channel to post alerts to :param message_prefix: optional message prefix to add before any message posted For example: message_prefix="Hey <!here>," :param filters: An optional collection of callables that will be passed a Task object and return True/False if it should be filtered away """ super(SlackMonitor, self).__init__() self.channel = "{}".format(channel[1:] if channel[0] == "#" else channel) self.slack_client = WebClient(token=slack_api_token) self.min_num_iterations = 0 self.filters = filters or list() self.status_alerts = [ "failed", ] self.include_manual_experiments = False self.include_archived = False self.verbose = False self._channel_id = None self._message_prefix = "{} ".format( message_prefix) if message_prefix else "" self.check_credentials() def check_credentials(self): # type: () -> () """ Check we have the correct credentials for the slack channel """ self.slack_client.api_test() # Find channel ID channels = [] cursor = None while True: response = self.slack_client.conversations_list(cursor=cursor) channels.extend(response.data["channels"]) cursor = response.data["response_metadata"].get("next_cursor") if not cursor: break channel_id = [ channel_info.get("id") for channel_info in channels if channel_info.get("name") == self.channel ] if not channel_id: raise ValueError( "Error: Could not locate channel name '{}'".format( self.channel)) # test bot permission (join channel) self._channel_id = channel_id[0] self.slack_client.conversations_join(channel=self._channel_id) def post_message(self, message, retries=1, wait_period=10.0): # type: (str, int, float) -> () """ Post message on our slack channel :param message: Message to be sent (markdown style) :param retries: Number of retries before giving up :param wait_period: wait between retries in seconds """ for i in range(retries): if i != 0: sleep(wait_period) try: self.slack_client.chat_postMessage( channel=self._channel_id, blocks=[ dict(type="section", text={ "type": "mrkdwn", "text": message }) ], ) return except SlackApiError as e: print( 'While trying to send message: "\n{}\n"\nGot an error: {}'. format(message, e.response["error"])) def get_query_parameters(self): # type: () -> dict """ Return the query parameters for the monitoring. :return dict: Example dictionary: {'status': ['failed'], 'order_by': ['-last_update']} """ filter_tags = list() if self.include_archived else ["-archived"] if not self.include_manual_experiments: filter_tags.append("-development") return dict(status=self.status_alerts, order_by=["-last_update"], system_tags=filter_tags) def process_task(self, task): """ # type: (Task) -> () Called on every Task that we monitor. This is where we send the Slack alert :return: None """ # skipping failed tasks with low number of iterations if self.min_num_iterations and task.get_last_iteration( ) < self.min_num_iterations: print("Skipping {} experiment id={}, number of iterations {} < {}". format(task.status, task.id, task.get_last_iteration(), self.min_num_iterations)) return if any(f(task) for f in self.filters): if self.verbose: print("Experiment id={} {} did not pass all filters".format( task.id, task.status)) return print('Experiment id={} {}, raising alert on channel "{}"'.format( task.id, task.status, self.channel)) console_output = task.get_reported_console_output(number_of_reports=3) message = "{}Experiment ID <{}|{}> *{}*\nProject: *{}* - Name: *{}*\n" "```\n{}\n```".format( self._message_prefix, task.get_output_log_web_page(), task.id, task.status, task.get_project_name(), task.name, ("\n".join(console_output))[-2048:], ) self.post_message(message, retries=5)
class ChannelManager(object): def __init__(self, token=Path('SLACK_OAUTH_TOKEN').read_text()): self.token = token self.client = WebClient(token=self.token) self.channels = [ ] # Sets the internal channels field def list(self): resp = self.client.conversations_list(exclude_archived=True, types="public_channel,private_channel", limit=500) assert resp.data['ok'] self.channels = [] for channel in resp.data['channels']: self.channels.append({ 'ID': channel['id'], 'Channel Name': channel['name'], 'Purpose': channel['purpose']['value'] }) def exists(self, name): for channel in self.channels: if channel['Channel Name'] == name: return True return False def get_id(self, name): for channel in self.channels: if channel['Channel Name'] == name: return channel['ID'] return False def get_purpose(self, name): for channel in self.channels: if channel['Channel Name'] == name: return channel['Purpose'] return False def create(self, name, is_private): print(f'Creating channel {name} ...') resp = self.client.conversations_create(name=name, is_private=is_private) return resp def set_purpose(self, name, purpose): chan_id = self.get_id(name) # TODO(arjun): Hack. The name that comes back has HTML entities for # special characters. God help us all. if self.get_purpose(name) != "": return print(f'Setting purpose for {name} ...') self.client.conversations_setPurpose(channel=chan_id, purpose=purpose) def post(self, name, message): channel_id = self.get_id(name) if not channel_id: return False try: resp = self.client.chat_postMessage(channel=channel_id, text=message) if resp.data['ok']: return True else: print(f"Response was {resp.data}") return False except SlackApiError as e: if e.response["error"] != "not_in_channel": raise e self.client.conversations_join(channel=channel_id) self.client.chat_postMessage(channel=channel_id, text=message) return True