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)
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)
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 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 _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)
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 _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()
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
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
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
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
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
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
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
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
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
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
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')
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)
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)
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
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
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)
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))
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)
def wait_available(pool, pool_name): statsd = stats.get_statsd_client() if pool.full(): statsd.incr('%s.pool.full' % pool_name) pool.wait_available()