Пример #1
0
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 {}
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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,
        )
Пример #5
0
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
Пример #6
0
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']},
                ),
            )
Пример #7
0
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)
Пример #8
0
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
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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 {}
Пример #12
0
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
Пример #13
0
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)
Пример #14
0
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
Пример #15
0
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
Пример #16
0
 def logging_context(self):
     return merge_logging_context(
         {
             'bot': self.name,
             'bot_id': self.bot_id,
         }, self.team.logging_context)