def get_channel(bot, channel): """ Get a channel, from its channel id """ logger.debug( 'Fetching channel', extra=merge_logging_context( {'channel': channel}, bot.logging_context, ) ) cached_channel = _get_channel_from_cache(bot, channel) if cached_channel: return cached_channel logger.debug( 'Channel not in cache.', extra=merge_logging_context( {'channel': channel}, bot.logging_context, ) ) channel_data = client(bot).api_call( 'conversations.info', channel=channel ) if channel_data['ok']: update_channel(bot, channel_data['channel']) return channel_data['channel'] return {}
def process_slash_command(command): """ Dispatcher for slack slash commands. """ statsd = stats.get_statsd_client() team = Team.get_team_by_id(command['team_id']) bot = Bot.get_bot_by_bot_id(team, command['omnibot_bot_id']) if command['command'].startswith('/'): command_name = command['command'][1:] else: command_name = command['command'] event_trace = merge_logging_context( { 'trigger_id': command['trigger_id'], 'command': command_name, }, bot.logging_context, ) statsd.incr('slash_command.process.attempt.{}'.format(command_name)) try: with statsd.timer('process_slash_command'): logger.debug('Processing slash_command: {}'.format( json.dumps(command, indent=2)), extra=event_trace) slash_command = SlashCommand(bot, command, event_trace) _process_slash_command_handlers(slash_command) except Exception: statsd.incr('slash_command.process.failed.{}'.format(command_name)) logger.exception('Could not process slash command.', exc_info=True, extra=event_trace)
def process_interactive_component(component): """ Dispatcher for slack interactive components """ statsd = stats.get_statsd_client() team = Team.get_team_by_id(component['team']['id']) bot = Bot.get_bot_by_bot_id(team, component['omnibot_bot_id']) event_trace = merge_logging_context( { 'callback_id': get_callback_id(component), 'component_type': component['type'], }, bot.logging_context, ) statsd.incr('interactive_component.process.attempt.{}'.format( get_callback_id(component))) try: with statsd.timer('process_interactive_component'): logger.debug('Processing interactive component: {}'.format( json.dumps(component, indent=2)), extra=event_trace) interactive_component = InteractiveComponent( bot, component, event_trace) _process_interactive_component(interactive_component) except Exception: statsd.incr('interactive_component.process.failed.{}'.format( get_callback_id(component))) logger.exception('Could not process interactive component.', exc_info=True, extra=event_trace)
def instrument_event(bot, event): statsd = stats.get_statsd_client() retry = request.headers.get('X-Slack-Retry-Num', default=0, type=int) retry_reason = request.headers.get('X-Slack-Retry-Reason', default='', type=str) event_info = event['event'] event_sent_time_ms = int(float(event_info['event_ts']) * 1000) now = int(time.time() * 1000) latency = now - event_sent_time_ms if retry > 0: statsd.timing('pre_sqs_delivery_retry_latency', latency) else: statsd.timing('pre_sqs_delivery_latency', latency) if latency > 20000: logger.warning('Event is greater than 20s delayed in' ' delivery ({} ms)'.format(latency), extra=merge_logging_context( { 'event_ts': event_info['event_ts'], 'event_type': event_info['type'], 'retry': retry }, bot.logging_context, )) if retry_reason: logger.warning( 'Incoming message is a retry: reason="{}"'.format(retry_reason), extra=bot.logging_context, )
def _get_emoji(bot): # TODO: split this retry logic into a generic retry function for retry in range(MAX_RETRIES): resp = client(bot).api_call('emoji.list') if resp['ok']: break logger.warning( 'Call to emoji.list failed, attempting retry', extra=merge_logging_context( {'retry': retry}, _get_failure_context(resp), bot.logging_context, ), ) gevent.sleep(GEVENT_SLEEP_TIME) else: logger.error( 'Exceeded max retries when calling emoji.list.', extra=bot.logging_context, ) return {} emoji = {} for k, v in resp['emoji'].items(): while v.startswith('alias:'): _, _, alias = v.partition(':') v = resp['emoji'].get(alias, '') if v: emoji[k] = v return emoji
def update_conversations(bot): for conversation in _get_conversations(bot): if conversation.get('is_channel', False): update_channel(bot, conversation) elif conversation.get('is_group', False): update_group(bot, conversation) elif conversation.get('is_im', False): update_im(bot, conversation) elif conversation.get('is_mpim', False): update_mpim(bot, conversation) else: logger.info( 'Not updating unsupported conversation.', extra=merge_logging_context( bot.logging_context, {'channel': conversation['id']}, ), )
def process_event(event): """ Dispatcher for slack api events. """ statsd = stats.get_statsd_client() team = Team.get_team_by_id(event['team_id']) bot = Bot.get_bot_by_bot_id(team, event['api_app_id']) event_info = event['event'] event_type = event_info['type'] event_trace = merge_logging_context( { 'event_ts': event_info['event_ts'], 'event_type': event_type, }, bot.logging_context, ) statsd.incr('event.process.attempt.{}'.format(event_type)) if event_type == 'message' or event_type == 'app_mention': try: with statsd.timer('process_event'): logger.debug( 'Processing message: {}'.format( json.dumps(event, indent=2) ), extra=event_trace ) try: message = Message(bot, event_info, event_trace) _process_message_handlers(message) except MessageUnsupportedError: pass except Exception: statsd.incr('event.process.failed.{}'.format(event_type)) logger.exception( 'Could not process message.', exc_info=True, extra=event_trace ) else: logger.debug( 'Event is not a message type.', extra=event_trace ) logger.debug(event)
def _get_conversations(bot): """ Get all conversations """ conversations = [] retry = 0 next_cursor = '' while True: conversations_data = client(bot).api_call( 'conversations.list', exclude_archived=True, exclude_members=True, limit=1000, cursor=next_cursor ) if conversations_data['ok']: conversations.extend(conversations_data['channels']) else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= MAX_RETRIES: logger.error( 'Exceeded max retries when calling conversations.list.', extra=bot.logging_context, ) break logger.warning( 'Call to channels.list failed, attempting retry', extra=merge_logging_context( {'retry': retry}, _get_failure_context(conversations_data), bot.logging_context, ), ) gevent.sleep(GEVENT_SLEEP_TIME) continue next_cursor = conversations_data.get( 'response_metadata', {} ).get('next_cursor') if not next_cursor: break gevent.sleep(GEVENT_SLEEP_TIME) return conversations
def _get_users(bot, max_retries=MAX_RETRIES, sleep=GEVENT_SLEEP_TIME): users = [] retry = 0 next_cursor = '' while True: users_data = client(bot).api_call( 'users.list', presence=False, limit=1000, cursor=next_cursor ) if users_data['ok']: users.extend(users_data['members']) else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= max_retries: logger.error( 'Exceeded max retries when calling users.list.', extra=bot.logging_context, ) break logger.warning( 'Call to users.list failed, attempting retry', extra=merge_logging_context( {'retry': retry}, _get_failure_context(users_data), bot.logging_context, ), ) gevent.sleep(sleep * retry) continue next_cursor = users_data.get( 'response_metadata', {} ).get('next_cursor') if not next_cursor: break gevent.sleep(sleep) return users
def get_im_channel_id(bot, user_id): redis_client = omniredis.get_redis_client() imsmap_id = redis_client.hget('imsmap:{}'.format(bot.team.name), user_id) if imsmap_id: raw_im = redis_client.hget('ims:{}'.format(bot.team.name), imsmap_id) if raw_im: im = json.loads(raw_im) if not im.get('is_user_deleted', False): return im['id'] retry = 0 while True: users = user_id conversation_data = client(bot).api_call( 'conversations.open', users=users ) if conversation_data['ok']: return conversation_data['channel']['id'] else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= MAX_RETRIES: logger.error( 'Exceeded max retries when calling conversations.open.', extra=bot.logging_context, ) break logger.warning( 'Call to conversations.open failed, attempting retry', extra=merge_logging_context( {'retry': retry}, _get_failure_context(conversation_data), bot.logging_context, ), ) gevent.sleep(GEVENT_SLEEP_TIME) continue return None
def get_user(bot, user_id): """ Get a user, from its user id """ redis_client = omniredis.get_redis_client() user = redis_client.hget('users:{}'.format(bot.team.name), user_id) if user: return json.loads(user) user = client(bot).api_call( 'users.info', user=user_id ) if user['ok']: update_user(bot, user['user']) return user['user'] else: logger.warning( 'Failed to find user', extra=merge_logging_context( {'user': user_id}, bot.logging_context, ), ) return {}
def _perform_action(bot, data): for arg in ['action', 'kwargs']: if arg not in data: return { 'ok': False, 'error': '{} not provided in payload'.format(arg) } action = data['action'] kwargs = data['kwargs'] logger.debug( 'Performing action', extra=merge_logging_context( {'action': action}, bot.logging_context, ), ) parse_kwargs(kwargs, bot) ret = slack.client(bot).api_call(action, **kwargs) logger.debug(ret) if not ret['ok']: if ret.get('error') in ['missing_scope', 'not_allowed_token_type']: logger.warning( 'action failed in post_slack, attempting as user.', extra=merge_logging_context( {'action': action}, bot.logging_context, ), ) try: ret = slack.client(bot, client_type='user').api_call( action, **kwargs) except json.decoder.JSONDecodeError: logger.exception( 'JSON decode failure when parsing kwargs={}'.format( kwargs), extra=merge_logging_context( {'action': action}, bot.logging_context, ), ) return {'ok': False} logger.debug(ret) if not ret['ok']: logger.error( 'action failed in post_slack: ret={}'.format(ret), extra=merge_logging_context( { 'action': action, 'kwargs': kwargs }, bot.logging_context, ), ) else: logger.error( 'action failed in post_slack: ret={}'.format(ret), extra=merge_logging_context( { 'action': action, 'kwargs': kwargs }, bot.logging_context, ), ) return ret
def get_channel_by_name(team_name, bot_name, channel_name): """ Returns a channel object from slack, for the provided `channel_name` in the `team_name` using the specified `bot_name`. .. :quickref: Channel; Get a channel from a team, via the channel_name **Example request**: .. sourcecode:: http GET /api/v1/slack/get_channel/myteam/mybot/general HTTP/1.1 **Example response**: .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "channel": { "id": "C4VQ6NUNN", "name": "general", "is_channel": true, "created": 1491515285, "creator": "U4WF56QGP", "is_archived": false, "is_general": true, "unlinked": 0, "name_normalized": "general", "is_shared": false, "is_org_shared": false, "is_member": false, "is_private": false, "is_mpim": false, "members": [ "U4WF56QGP", "U6HQQ19EC", "U6J3LTKSQ", "U6J4EGP44", "U6JDF1JBU", "U6JEGTFDZ", "U6JERPMJ7", "U6JG691MJ", "U6JGEQ0J0", "U6SAVUK44", "U750C7B37", "U7DH0H802" ], "topic": { "value": "test123", "creator": "U6J3LTKSQ", "last_set": 1507156612 }, "purpose": { "value": "This channel is for team-wide communication.", "creator": "", "last_set": 0 }, "previous_names": [], "num_members": 9 } } :param team_name: The team to search for this channel, as configured in omnibot. :type team_name: str :param bot_name: The bot to use for the request, as configured in omnibot. :type bot_name: str :param channel_name: The name of the channel to get. :type channel_name: str :reqheader x-envoy-internal: Header that indicates whether or not this request is coming from an internal service or not. This is auto-set by envoy and doesn't need to be explicitly set. :resheader Content-Type: application/json :statuscode 200: success :statuscode 404: channel with specified channel_name could not be found in the specified team using the specified bot. """ logger.debug( 'Getting channel for team={} bot={} channel={}.', extra={ 'team': team_name, 'bot': bot_name, 'channel': channel_name, }, ) try: team = Team.get_team_by_name(team_name) except TeamInitializationError: return jsonify({'error': 'provided team name was not found.'}), 404 try: bot = Bot.get_bot_by_name(team, bot_name) except BotInitializationError: return jsonify({'error': 'provided bot name was not found.'}), 404 channel = slack.get_channel_by_name(bot, channel_name) if channel is None: logger.debug( 'Failed to get channel', extra=merge_logging_context( {'channel': channel_name}, bot.logging_context, ), ) return jsonify({'error': 'provided channel_name was not found.'}), 404 return jsonify(channel)
def slack_interactive_component(): # Slack sends interactive components as application/x-www-form-urlencoded, # json encoded inside of the payload field. What a whacky API. component = json.loads(request.form.to_dict().get('payload', {})) logger.debug( 'component received in API slack_slash_command: {}'.format(component)) if (component.get('type') not in [ 'interactive_message', 'message_action', 'dialog_submission', 'block_actions', ]): msg = ('Unsupported type={} in interactive' ' component.'.format(component.get('type'))) logger.warning(msg) return jsonify({'status': 'failure', 'error': msg}), 400 # Every event should have a validation token if 'token' not in component: msg = 'No verification token in interactive component.' logger.warning(msg) return jsonify({'status': 'failure', 'error': msg}), 403 if not component.get('team', {}).get('id'): msg = 'No team id in interactive component.' logger.warning(msg) return jsonify({'status': 'failure', 'error': msg}), 403 try: team = Team.get_team_by_id(component['team']['id']) except TeamInitializationError: msg = 'Unsupported team' logger.warning( msg, extra={'team_id': component['team']['id']}, ) return jsonify({'status': 'failure', 'error': msg}), 403 # interactive components annoyingly don't send an app id, so we need # to verify try: bot = Bot.get_bot_by_verification_token(component['token']) except BotInitializationError: msg = ('Token sent with interactive component does not match any' ' configured app.') logger.error( msg, extra=team.logging_context, ) return jsonify({'status': 'failure', 'error': msg}), 403 if team.team_id != bot.team.team_id: # This should never happen, but let's be paranoid. msg = ('Token sent with slash command does not match team in event.') logger.error( msg, extra=merge_logging_context( {'expected_team_id': team.team_id}, bot.logging_context, ), ) return jsonify({'status': 'failure', 'error': msg}), 403 handler_found = None for handler in bot.interactive_component_handlers: if get_callback_id(component) == handler.get('callback_id'): handler_found = handler break if not handler_found: msg = ('This interactive component does not have any omnibot handler' ' associated with it.') logger.error( msg, extra=bot.logging_context, ) return jsonify({'response_type': 'ephemeral', 'text': msg}), 200 # To avoid needing to look the bot up from its token when the dequeue this # command,:let's extend the payload with the bot id component['omnibot_bot_id'] = bot.bot_id # TODO: Use action_ts to instrument event try: # If there's no callbacks defined for this interactive component, we # can skip enqueuing it, since the workers will just discard it. if handler_found.get('callbacks'): queue_event(bot, component, 'interactive_component') except Exception: msg = 'Could not queue interactive component.' logger.exception( msg, extra=bot.logging_context, ) return jsonify({'status': 'failure', 'error': msg}), 500 # Open a dialog, if we have a trigger ID, and a dialog is defined for this # handler. Not all interactive components have a trigger ID. if component.get('trigger_id') and handler_found.get('dialog'): _perform_action( bot, { 'action': 'dialog.open', 'kwargs': { 'dialog': handler_found['dialog'], 'trigger_id': component['trigger_id'] } }) if component['type'] in ['dialog_submission']: return '', 200 elif handler_found.get('no_message_response'): return '', 200 else: return _get_write_message_response(handler_found), 200
def slack_slash_command(): # Slack sends slash commands as application/x-www-form-urlencoded command = request.form.to_dict() logger.debug( 'command received in API slack_slash_command: {}'.format(command)) # Every event should have a validation token if 'token' not in command: msg = 'No verification token in slash command.' logger.error(msg) return jsonify({'status': 'failure', 'error': msg}), 403 if 'team_id' not in command: msg = 'No team_id in slash command.' logger.error(msg) return jsonify({'status': 'failure', 'error': msg}), 403 try: team = Team.get_team_by_id(command['team_id']) except TeamInitializationError: msg = 'Unsupported team' logger.warning( msg, extra={'team_id': command['team_id']}, ) return jsonify({'status': 'failure', 'error': msg}), 403 # Slash commands annoyingly don't send an app id, so we need to verify try: bot = Bot.get_bot_by_verification_token(command['token']) except BotInitializationError: msg = ( 'Token sent with slash command does not match any configured app.') logger.error( msg, extra=team.logging_context, ) return jsonify({'status': 'failure', 'error': msg}), 403 if team.team_id != bot.team.team_id: # This should never happen, but let's be paranoid. msg = ('Token sent with slash command does not match team in event.') logger.error(msg, extra=merge_logging_context( {'expected_team_id': team.team_id}, bot.logging_context, )) return jsonify({'status': 'failure', 'error': msg}), 403 handler_found = None for slash_handler in bot.slash_command_handlers: if command['command'] == slash_handler.get('command'): handler_found = slash_handler break if not handler_found: msg = ('This slash command does not have any omnibot handler' ' associated with it.') logger.error( msg, extra=bot.logging_context, ) return jsonify({'response_type': 'ephemeral', 'text': msg}), 200 # To avoid needing to look the bot up from its token when the dequeue this # command,:let's extend the payload with the bot id command['omnibot_bot_id'] = bot.bot_id # We can't instrument slash commands, because they don't have ts info. # TODO: investigate if we can parse the trigger ID; it's possible part # of that is a timestamp try: # If there's no callbacks defined for this slash command, we # can skip enqueuing it, since the workers will just discard it. if handler_found.get('callbacks'): queue_event(bot, command, 'slash_command') except Exception: msg = 'Could not queue slash command.' logger.exception( msg, extra={ 'team': team.team_id, 'app': bot.bot_id, 'bot': bot.name }, ) return jsonify({'status': 'failure', 'error': msg}), 500 if handler_found.get('dialog'): _perform_action( bot, { 'action': 'dialog.open', 'kwargs': { 'dialog': handler_found['dialog'], 'trigger_id': command['trigger_id'] } }) return _get_write_message_response(handler_found), 200
def logging_context(self): return merge_logging_context( { 'bot': self.name, 'bot_id': self.bot_id, }, self.team.logging_context)