Ejemplo n.º 1
0
def watch_channels():
    try:
        redis_client = omniredis.get_redis_client(decode_responses=False)
        last_run_key = "watch:channels:last_run_datetime"
        if not _is_allowed_to_run(redis_client, last_run_key):
            return

        statsd = stats.get_statsd_client()
        with redis_lock.Lock(redis_client,
                             'watch_channels',
                             expire=LOCK_EXPIRATION,
                             auto_renewal=True):
            with statsd.timer('watch.channels'):
                for team_name, bot_name in settings.PRIMARY_SLACK_BOT.items():
                    logger.info(
                        'Updating slack channel list.',
                        extra={
                            'team': team_name,
                            'bot': bot_name
                        },
                    )
                    team = Team.get_team_by_name(team_name)
                    bot = Bot.get_bot_by_name(team, bot_name)
                    slack.update_channels(bot)
            redis_client.set(last_run_key, datetime.now().isoformat())
    except Exception:
        logger.exception('Failed to update slack channel list.', exc_info=True)
    finally:
        return gevent.spawn_later(settings.WATCHER_SPAWN_WAIT_TIME_IN_SEC,
                                  watch_channels)
Ejemplo n.º 2
0
def handle_messages(client, queue_url, queue_pool):
    global STATE
    statsd = stats.get_statsd_client()

    while not STATE['shutdown']:
        try:
            response = client.receive_message(
                QueueUrl=queue_url,
                AttributeNames=['SentTimestamp'],
                MaxNumberOfMessages=settings.SQS_BATCH_SIZE,
                MessageAttributeNames=['All'],
                VisibilityTimeout=settings.SQS_VISIBILITY_TIMEOUT,
                WaitTimeSeconds=settings.SQS_WAIT_TIME_SECONDS)
            if 'Messages' in response:
                statsd.incr('sqs.received', len(response['Messages']))
                for message in response['Messages']:
                    with statsd.timer('webhookpool.spawn'):
                        wait_available(queue_pool, 'webhookpool')
                        queue_pool.spawn(handle_message, client, queue_url,
                                         message)
            else:
                logger.debug('No messages, continuing')
        except botocore.parsers.ResponseParserError:
            logger.warning('Got a bad response from SQS, continuing.')
        except Exception:
            logger.exception('General error', exc_info=True)
Ejemplo n.º 3
0
Archivo: api.py Proyecto: lyft/omnibot
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,
        )
Ejemplo n.º 4
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)
Ejemplo n.º 5
0
def _process_message_handlers(message):
    bot = message.bot
    statsd = stats.get_statsd_client()
    command_matched = False
    handler_called = False
    for handler in bot.message_handlers:
        # We only match commands against directed messages
        if handler['match_type'] == 'command':
            if not _should_handle_command(handler, message):
                continue
            # We only match against a single command
            if command_matched:
                continue
            if message.command_text.startswith(handler['match']):
                command_matched = True
                message.set_match('command', handler['match'])
                for callback in handler['callbacks']:
                    _handle_message_callback(message, callback)
                    handler_called = True
        if handler['match_type'] == 'regex':
            match = bool(re.search(handler['match'], message.parsed_text))
            regex_should_not_match = handler.get('regex_type') == 'absence'
            # A matched regex should callback only if the regex is supposed to
            # match. An unmatched regex should callback only if the regex is
            # not supposed to match.
            if match != regex_should_not_match:
                message.set_match('regex', handler['match'])
                for callback in handler['callbacks']:
                    _handle_message_callback(message, callback)
                    handler_called = True
    if handler_called:
        statsd.incr('event.handled')
    elif not handler_called:
        _handle_help(message)
Ejemplo n.º 6
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)
Ejemplo n.º 7
0
 def _check_unsupported(self):
     # TODO: make the ignores configurable, but have a default list
     # Ignore self
     # Ignore bots
     unsupported = False
     if self.bot_id:
         logger.debug('ignoring message from bot', extra=self.event_trace)
         unsupported = True
     # Ignore threads
     elif self.thread_ts:
         logger.debug('ignoring thread message', extra=self.event_trace)
         unsupported = True
     # For now, ignore all event subtypes
     elif self.subtype:
         extra = {'subtype': self.subtype}
         extra.update(self.event_trace)
         logger.debug(
             'ignoring message with unsupported subtype',
             extra=extra,
         )
         unsupported = True
     if unsupported:
         statsd = stats.get_statsd_client()
         statsd.incr('event.unsupported')
         raise MessageUnsupportedError()
Ejemplo n.º 8
0
def unextract_specials(text):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.unextract_specials'):
        # Example: @here
        specials = re.findall('(@here|@channel)', text)
        for special in specials:
            text = text.replace(special, '<!{0}|{0}>'.format(special[1:]))
        return text
Ejemplo n.º 9
0
def extract_mentions(text, bot, channel):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_mentions'):
        to_me = False
        at_me = '@{}'.format(bot.name)
        if text.split(' ')[0] == at_me:
            to_me = True
        directed = channel.get('is_im') or to_me
        return directed
Ejemplo n.º 10
0
def extract_command(text, bot):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_command'):
        at_me = '@{}'.format(bot.name)
        if text.startswith(at_me):
            command_text = text[len(at_me):].strip()
        elif at_me in text:
            command_text = re.sub(r'.*{}'.format(at_me), '', text).strip()
        else:
            command_text = text
        return command_text
Ejemplo n.º 11
0
def extract_urls(text):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_urls'):
        # Example: <http://test.com> or <http://test.com|test.com>
        # [^>]* is non-greedy .*
        urls = re.findall('<(http[s]?://[^>]*)(?:\|[^>]*)?>', text)
        url_arr = {}
        for url in urls:
            unparsed_url = '<{0}>'.format(url)
            url_label = url.split('|')[0]
            url_arr[unparsed_url] = url_label
        return url_arr
Ejemplo n.º 12
0
def unextract_channels(text, bot):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.unextract_channels'):
        # Example: #my-channel
        _channel_labels = re.findall('(^#[\w\-_]+| #[\w\-_]+)', text)
        for label in _channel_labels:
            channel = slack.get_channel_by_name(bot, label.strip())
            if not channel:
                continue
            text = text.replace(
                '#{}'.format(channel['name']),
                '<#{0}|{1}>'.format(channel['id'], channel['name']))
        return text
Ejemplo n.º 13
0
def extract_emojis(text):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_emojis'):
        # Example: :test_me: or :test-me:
        emojis = re.findall(':[a-z0-9_\+\-]+:', text)
        emoji_arr = {}
        for emoji in emojis:
            match = re.match(':([a-z0-9_\+\-]+):', emoji)
            emoji_name = None
            if match.group(1) is not None:
                emoji_name = match.group(1)
            emoji_arr[emoji] = emoji_name
        return emoji_arr
Ejemplo n.º 14
0
def extract_specials(text):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_specials'):
        # Example: <!here|@here>
        specials = re.findall('<!\w+(?:\|@[\w-]+)?>', text)
        special_arr = {}
        for special in specials:
            match = re.match('<!(\w+)(?:\|@[\w-]+)?>', special)
            special_label = None
            if match.group(1) is not None:
                special_label = '@{}'.format(match.group(1))
            special_arr[special] = special_label
        return special_arr
Ejemplo n.º 15
0
def extract_subteams(text, bot):
    statsd = stats.get_statsd_client()
    # TODO: parse this
    with statsd.timer('parser.extract_subteams'):
        # Example: <!subteam^S012345|happy-peeps>
        # subteams = re.findall(
        #     '<!subteam\^S\w+(?:\|@[\w-]+)?>',
        #     metadata['text']
        # )
        subteam_arr = {}
        # for subteam in subteams:
        #     metadata['subteams'][subteam] = None
        return subteam_arr
Ejemplo n.º 16
0
def extract_emails(text):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_emails'):
        # Example: <mailto:[email protected]|[email protected]>
        emails = re.findall(
            # [^>]* is non-greedy .*
            '<mailto:([^>]*)(?:\|[^>]*)?>',
            text)
        email_arr = {}
        for email in emails:
            unparsed_email = '<mailto:{0}>'.format(email)
            email_label = email.split('|')[0]
            email_arr[unparsed_email] = email_label
        return email_arr
Ejemplo n.º 17
0
def unextract_users(text, bot):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.unextract_users'):
        # Example: @my-user
        _user_labels = re.findall('(^@[\w\-_]+| @[\w\-_]+)', text)
        user_labels = []
        for label in _user_labels:
            user_labels.append(label.strip())
        for label in user_labels:
            user = slack.get_user_by_name(bot, label)
            if not user:
                continue
            text = text.replace(
                label, '<@{0}|{1}>'.format(user['id'],
                                           slack.get_name_from_user(user)))
        return text
Ejemplo n.º 18
0
def _handle_help(message):
    statsd = stats.get_statsd_client()
    if message.directed:
        statsd.incr('event.defaulted')
        if settings.HELP_CALLBACK:
            _handle_message_callback(message,
                                     settings.HELP_CALLBACK['callback'])
        elif settings.DEFAULT_TO_HELP:
            _handle_message_callback(message, {
                'module':
                'omnibot.callbacks.message_callbacks:help_callback'
            })
        else:
            # TODO: respond with error message here
            pass
    else:
        statsd.incr('event.ignored')
Ejemplo n.º 19
0
def parse_kwargs(kwargs, bot, event_trace=None):
    if event_trace is None:
        event_trace = {}
    statsd = stats.get_statsd_client()
    omnibot_parse = kwargs.pop('omnibot_parse', {})
    for attr, to_parse in omnibot_parse.items():
        if attr not in kwargs:
            logger.warning(
                '{} not found in kwargs when parsing post response.'.format(
                    attr),
                extra=event_trace)
        with statsd.timer('unexpand_metadata'):
            if 'specials' in to_parse:
                kwargs[attr] = parser.unextract_specials(kwargs[attr])
            if 'channels' in to_parse:
                kwargs[attr] = parser.unextract_channels(kwargs[attr], bot)
            if 'users' in to_parse:
                kwargs[attr] = parser.unextract_users(kwargs[attr], bot)
Ejemplo n.º 20
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 = {
        'event_ts': event_info['event_ts'],
        'event_type': event_type,
        'app_id': event['api_app_id'],
        'team_id': bot.team.team_id,
        'bot_receiver': bot.name
    }
    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)
Ejemplo n.º 21
0
def extract_users(text, bot):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_users'):
        # Example: <@U024BE7LH> or <@U024BE7LH|bob-marley> or <@W024BE7LH|bob-marley>
        user_arr = {}
        users = re.findall('<@[UW]\w+(?:\|[\w-]+)?>', text)
        for user in users:
            match = re.match('<@([UW]\w+)(\|[\w-]+)?>', user)
            user_name = None
            if match.group(2) is not None:
                # user name is embedded; use the second match and strip |
                user_name = match.group(2)[1:]
            else:
                user_id = match.group(1)
                user_data = slack.get_user(bot, user_id)
                if user_data:
                    user_name = user_data['name']
            user_arr[user] = user_name
        return user_arr
Ejemplo n.º 22
0
def extract_channels(text, bot):
    statsd = stats.get_statsd_client()
    with statsd.timer('parser.extract_channels'):
        # Example: <#C024BE7LR> or <#C024BE7LR|general-room>
        channel_arr = {}
        channels = re.findall('<#C\w+(?:\|[\w-]+)?>', text)
        for channel in channels:
            match = re.match('<#(C\w+)(\|[\w-]+)?>', channel)
            channel_name = None
            if match.group(2) is not None:
                # channel name is embedded; use the second match and strip |
                channel_name = match.group(2)[1:]
            else:
                channel_id = match.group(1)
                channel_data = slack.get_channel(bot, channel_id)
                if not channel_data:
                    continue
                channel_name = channel_data['name']
            channel_arr[channel] = channel_name
        return channel_arr
Ejemplo n.º 23
0
def handle_message(client, queue_url, message):
    statsd = stats.get_statsd_client()
    with statsd.timer('handle_message'):
        attrs = message['MessageAttributes']
        if 'type' not in attrs:
            logger.error('SQS message does not have a type attribute.')
            delete_message(client, queue_url, message)
            return
        m_type = attrs['type']['StringValue']
        if m_type not in ['event', 'slash_command', 'interactive_component']:
            delete_message(client, queue_url, message)
            logger.error('{} is an unsupported message type.'.format(m_type))
            return
        if 'version' not in attrs:
            version = 1
        else:
            version = int(attrs['version']['StringValue'])
        logger.debug('Received SQS message of type {}'.format(m_type))
        try:
            if version == 2:
                event = json.loads(message['Body'])['event']
                if m_type == 'event':
                    _instrument_message_latency(event['event'])
                    processor.process_event(event)
                elif m_type == 'slash_command':
                    processor.process_slash_command(event)
                elif m_type == 'interactive_component':
                    processor.process_interactive_component(event)
            else:
                logger.error(
                    '{} is an unsupported message version.'.format(version))
        except Exception:
            logger.exception('Failed to handle webhook SQS message',
                             exc_info=True)
            return
        delete_message(client, queue_url, message)
Ejemplo n.º 24
0
Archivo: api.py Proyecto: lyft/omnibot
def queue_event(bot, event, event_type):
    statsd = stats.get_statsd_client()
    sqs_client = sqs.get_client()
    sqs_client.send_message(
        QueueUrl=sqs.get_queue_url(),
        MessageBody=json.dumps({'event': event}),
        MessageAttributes={
            # Add a version, so we know how to parse this in the receiver when
            # we make message schema changes.
            'version': {
                'DataType': 'Number',
                # Seems SQS uses StringValue for Number type... We'll cast
                # this on the receiver end.
                'StringValue': '2'
            },
            # Specify the type of SQS message, so we can handle more than just
            # the event subscription API.
            'type': {
                'DataType': 'String',
                'StringValue': event_type
            }
        })
    statsd.incr('sqs.sent')
    statsd.incr('sqs.{}.sent'.format(bot.name))
Ejemplo n.º 25
0
def _instrument_message_latency(event):
    statsd = stats.get_statsd_client()
    event_sent_time_ms = int(float(event['event_ts']) * 1000)
    now = int(time.time() * 1000)
    statsd.timing('delivery_latency', now - event_sent_time_ms)
Ejemplo n.º 26
0
def wait_available(pool, pool_name):
    statsd = stats.get_statsd_client()
    if pool.full():
        statsd.incr('%s.pool.full' % pool_name)
        pool.wait_available()