package com.hubspot.blazar.util; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.MetricRegistry; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; import com.github.rholder.retry.WaitStrategies; import com.google.common.base.Optional; import com.google.common.base.Predicates; import com.google.inject.Inject; import com.google.inject.Singleton; import com.ullink.slack.simpleslackapi.SlackAttachment; import com.ullink.slack.simpleslackapi.SlackChannel; import com.ullink.slack.simpleslackapi.SlackMessageHandle; import com.ullink.slack.simpleslackapi.SlackSession; import com.ullink.slack.simpleslackapi.SlackUser; import com.ullink.slack.simpleslackapi.replies.ParsedSlackReply; import com.ullink.slack.simpleslackapi.replies.SlackMessageReply; /** * This class handles all the details of sending a slack message, and verifying that it did send. */ @Singleton public class BlazarSlackClient { private static final Logger LOG = LoggerFactory.getLogger(BlazarSlackClient.class); private static final Retryer<Boolean> RETRYER = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(Predicates.equalTo(Boolean.FALSE)) .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build(); private SlackSession session; private MetricRegistry metricRegistry; @Inject public BlazarSlackClient(SlackSession session, MetricRegistry metricRegistry) { this.session = session; this.metricRegistry = metricRegistry; } public Set<com.hubspot.blazar.externalservice.slack.SlackChannel> getChannels() { Collection<SlackChannel> channels = ensureSlackSessionConnectedAndFetchData(() -> session.getChannels(), Collections.emptyList()); return channels.stream().map(channel -> new com.hubspot.blazar.externalservice.slack.SlackChannel(channel.getId(), channel.getName())).collect(Collectors.toSet()); } public void sendMessageToChannel(String channelName, String message, SlackAttachment attachment) { Optional<SlackChannel> slackChannel = ensureSlackSessionConnectedAndFetchData(() -> session.findChannelByName(channelName)); if (!slackChannel.isPresent()) { LOG.warn("No slack channel found for name {}", channelName); return; } sendMessageToChannel(slackChannel.get(), message, attachment); } public void sendMessageToChannel(SlackChannel channel, String message, SlackAttachment attachment) { try { RETRYER.call(() -> sendMessage(channel, message, attachment)); metricRegistry.counter("successful-slack-channel-sends").inc(); // Here we swallow exceptions that might be thrown by our retryer or #sendMessage(). // We don't catch and retry any exceptions because the slack session doesn't throw us any, (it swallows them and returns null) } catch (Exception e) { metricRegistry.counter("failed-slack-channel-sends").inc(); LOG.error("Could not send slack message {}", attachment, e); } } public void sendMessageToUser(String email, String message, SlackAttachment attachment) { Optional<SlackUser> user = ensureSlackSessionConnectedAndFetchData(() -> session.findUserByEmail(email)); if (!user.isPresent()) { LOG.warn("Could not find user with email {} ", email); return; } sendMessageToUser(user.get(), message, attachment); } public void sendMessageToUser(SlackUser user, String message, SlackAttachment attachment) { try { RETRYER.call(() -> sendMessage(user, message, attachment)); metricRegistry.counter("successful-slack-dm-sends").inc(); // Here we swallow exceptions that might be thrown by our retryer or #sendMessage(). // We don't catch and retry any exceptions because the slack session doesn't throw us any, (it swallows them and returns null) } catch (Exception e) { LOG.error("Could not send slack message {}", attachment, e); metricRegistry.counter("failed-slack-dm-sends").inc(); } } private boolean sendMessage(SlackUser user, String message, SlackAttachment attachment) { Optional<SlackMessageHandle<SlackMessageReply>> result = ensureSlackSessionConnectedAndFetchData(() -> session.sendMessageToUser(user, message, attachment)); if (!result.isPresent()) { LOG.warn("Failed to send slack message to user: {} message: {} slack result was null", user.getRealName(), attachment.toString()); return false; } ParsedSlackReply reply = result.get().getReply(); if (!reply.isOk()) { LOG.warn("Failed to send slack message to user: {} message: {} error: {}", user.getRealName(), attachment.toString(), reply.getErrorMessage()); return false; } return true; } private boolean sendMessage(SlackChannel channel, String message, SlackAttachment attachment) { Optional<SlackMessageHandle<SlackMessageReply>> result = ensureSlackSessionConnectedAndFetchData(() -> session.sendMessage(channel, message, attachment)); // This slack library does not throw us back any exceptions it just calls `e.printStackTrace()` and returns null if (!result.isPresent()) { LOG.warn("Failed to send slack message to channel: {} message: {} slack result was null", channel.getName(), attachment.toString()); return false; } ParsedSlackReply reply = result.get().getReply(); if (!reply.isOk()) { if (reply.getErrorMessage().equals("not_in_channel")) { // There is nothing the bot can do about this. It must be invited returning `true` here prevents sentries etc. return true; } else if (reply.getErrorMessage().equals("is_archived")) { // ignore errors sending to archived channels return true; } LOG.warn("Failed to send slack message to channel: {} message: {} error: {}", channel.getName(), attachment.toString(), reply.getErrorMessage()); return false; } return true; } private <T> T ensureSlackSessionConnectedAndFetchData(Supplier<T> slackDataSupplier, T emptyValue) { if (!session.isConnected()) { try { session.connect(); return slackDataSupplier.get(); } catch (IOException e) { LOG.error("Could not connect to slack: {}", e); return emptyValue; } } return slackDataSupplier.get(); } private <T> Optional<T> ensureSlackSessionConnectedAndFetchData(Supplier<T> slackDataSupplier) { return ensureSlackSessionConnectedAndFetchData(() -> Optional.fromNullable(slackDataSupplier.get()), Optional.absent()); } }