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 __init__(self, bot, component, event_trace): self._event_trace = event_trace self._payload = {} self._payload['omnibot_payload_type'] = 'interactive_component' self._bot = bot # The bot object has data we don't want to pass to downstreams, so # in the payload, we just store specific bot data. self._payload['bot'] = {'name': bot.name, 'bot_id': bot.bot_id} # For future safety sake, we'll do the same for the team. self._payload['team'] = { 'name': bot.team.name, 'team_id': bot.team.team_id } self._payload['type'] = component['type'] self._payload['callback_id'] = get_callback_id(component) self._payload['action_ts'] = component.get('action_ts') self._payload['message_ts'] = component.get('message_ts') self._payload['trigger_id'] = component.get('trigger_id') self._payload['response_url'] = component['response_url'] self._payload['original_message'] = component.get('original_message') self._payload['state'] = component.get('state') self._payload['user'] = component.get('user') if self.user: self._payload['parsed_user'] = slack.get_user( self.bot, self.user['id']) self._payload['channel'] = component.get('channel') if self.channel: self._event_trace['channel_id'] = self.channel['id'] self._payload['parsed_channel'] = slack.get_channel( self.bot, self.channel['id']) self._payload['message'] = component.get('message') if self.message: self._parse_message() self._payload['submission'] = component.get('submission') self._payload['actions'] = component.get('actions')
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