Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send an approved public kudos to the #kudos slack channel. #2776

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dependencies {
implementation("io.micrometer:context-propagation")

implementation 'ch.digitalfondue.mjml4j:mjml4j:1.0.3'
implementation("com.slack.api:slack-api-client:1.44.1")

testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.objectcomputing.checkins.notifications.social_media;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;

import jakarta.inject.Singleton;
import jakarta.inject.Inject;

import java.util.List;

@Singleton
public class SlackPoster {
@Inject
private HttpClient slackClient;

public HttpResponse post(String slackBlock) {
// See if we can have a webhook URL.
String slackWebHook = System.getenv("SLACK_WEBHOOK_URL");
if (slackWebHook != null) {
// POST it to Slack.
BlockingHttpClient client = slackClient.toBlocking();
HttpRequest<String> request = HttpRequest.POST(slackWebHook,
slackBlock);
return client.exchange(request);
}
return HttpResponse.status(HttpStatus.GONE,
"Slack Webhook URL is not configured");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.objectcomputing.checkins.notifications.social_media;

import com.slack.api.model.block.LayoutBlock;
import com.slack.api.Slack;
import com.slack.api.methods.MethodsClient;
import com.slack.api.model.Conversation;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.conversations.ConversationsListRequest;
import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.request.users.UsersLookupByEmailRequest;
import com.slack.api.methods.response.users.UsersLookupByEmailResponse;

import jakarta.inject.Singleton;
import jakarta.inject.Inject;

import java.util.List;
import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class SlackSearch {
private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class);
private static final String env = "SLACK_BOT_TOKEN";

public String findChannelId(String channelName) {
String token = System.getenv(env);
if (token != null) {
try {
MethodsClient client = Slack.getInstance().methods(token);
ConversationsListResponse response = client.conversationsList(
ConversationsListRequest.builder().build()
);

if (response.isOk()) {
for (Conversation conversation: response.getChannels()) {
if (conversation.getName().equals(channelName)) {
return conversation.getId();
}
}
}
} catch(IOException e) {
LOG.error("SlackSearch.findChannelId: " + e.toString());
} catch(SlackApiException e) {
LOG.error("SlackSearch.findChannelId: " + e.toString());
}
}
return null;
}

public String findUserId(String userEmail) {
String token = System.getenv(env);
if (token != null) {
try {
MethodsClient client = Slack.getInstance().methods(token);
UsersLookupByEmailResponse response = client.usersLookupByEmail(
UsersLookupByEmailRequest.builder().email(userEmail).build()
);

if (response.isOk()) {
return response.getUser().getId();
}
} catch(IOException e) {
LOG.error("SlackSearch.findUserId: " + e.toString());
} catch(SlackApiException e) {
LOG.error("SlackSearch.findUserId: " + e.toString());
}
}
return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.objectcomputing.checkins.services.kudos;

import com.objectcomputing.checkins.notifications.social_media.SlackPoster;
import com.objectcomputing.checkins.notifications.social_media.SlackSearch;
import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServices;
import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils;
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;

import com.slack.api.model.block.LayoutBlock;
import com.slack.api.model.block.RichTextBlock;
import com.slack.api.model.block.element.RichTextElement;
import com.slack.api.model.block.element.RichTextSectionElement;
import com.slack.api.util.json.GsonFactory;
import com.google.gson.Gson;

import java.util.UUID;
import java.util.List;
import java.util.ArrayList;

public class KudosConverter {
private record InternalBlock(
List<LayoutBlock> blocks
) {}

private final MemberProfileServices memberProfileServices;
private final KudosRecipientServices kudosRecipientServices;

public KudosConverter(MemberProfileServices memberProfileServices,
KudosRecipientServices kudosRecipientServices) {
this.memberProfileServices = memberProfileServices;
this.kudosRecipientServices = kudosRecipientServices;
}

public String toSlackBlock(Kudos kudos) {
// Build the message text out of the Kudos data.
List<RichTextElement> content = new ArrayList<>();

// Look up the channel id from Slack
String channelName = "kudos";
SlackSearch search = new SlackSearch();
String channelId = search.findChannelId(channelName);
if (channelId == null) {
content.add(
RichTextSectionElement.Text.builder()
.text("#" + channelName)
.style(boldItalic())
.build()
);
} else {
content.add(
RichTextSectionElement.Channel.builder()
.channelId(channelId)
.style(limitedBoldItalic())
.build()
);
}
content.add(
RichTextSectionElement.Text.builder()
.text(" from ")
.style(boldItalic())
.build()
);
content.add(memberAsRichText(kudos.getSenderId()));
content.addAll(recipients(kudos));

content.add(
RichTextSectionElement.Text.builder()
.text("\n" + kudos.getMessage() + "\n")
.style(boldItalic())
.build()
);

// Bring it all together.
RichTextSectionElement element = RichTextSectionElement.builder()
.elements(content).build();
RichTextBlock richTextBlock = RichTextBlock.builder()
.elements(List.of(element)).build();
InternalBlock block = new InternalBlock(List.of(richTextBlock));
Gson mapper = GsonFactory.createSnakeCase();
return mapper.toJson(block);
}

private RichTextSectionElement.TextStyle boldItalic() {
return RichTextSectionElement.TextStyle.builder()
.bold(true).italic(true).build();
}

private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() {
return RichTextSectionElement.LimitedTextStyle.builder()
.bold(true).italic(true).build();
}

private RichTextElement memberAsRichText(UUID memberId) {
// Look up the user id by email address on Slack
SlackSearch search = new SlackSearch();
MemberProfile profile = memberProfileServices.getById(memberId);
String userId = search.findUserId(profile.getWorkEmail());
if (userId == null) {
String name = MemberProfileUtils.getFullName(profile);
return RichTextSectionElement.Text.builder()
.text("@" + name)
.style(boldItalic())
.build();
} else {
return RichTextSectionElement.User.builder()
.userId(userId)
.style(limitedBoldItalic())
.build();
}
}

private List<RichTextElement> recipients(Kudos kudos) {
List<RichTextElement> list = new ArrayList<>();
List<KudosRecipient> recipients =
kudosRecipientServices.getAllByKudosId(kudos.getId());
String separator = " to ";
for (KudosRecipient recipient : recipients) {
list.add(RichTextSectionElement.Text.builder()
.text(separator)
.style(boldItalic())
.build());
list.add(memberAsRichText(recipient.getMemberId()));
separator = ", ";
}
return list;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
import com.objectcomputing.checkins.notifications.email.EmailSender;
import com.objectcomputing.checkins.notifications.email.MailJetFactory;
import com.objectcomputing.checkins.notifications.social_media.SlackPoster;
import com.objectcomputing.checkins.exceptions.BadArgException;
import com.objectcomputing.checkins.exceptions.NotFoundException;
import com.objectcomputing.checkins.exceptions.PermissionException;
Expand All @@ -21,6 +22,9 @@
import com.objectcomputing.checkins.util.Util;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.transaction.annotation.Transactional;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;

import jakarta.inject.Named;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
Expand Down Expand Up @@ -49,6 +53,7 @@ class KudosServicesImpl implements KudosServices {
private final CheckInsConfiguration checkInsConfiguration;
private final RoleServices roleServices;
private final MemberProfileServices memberProfileServices;
private final SlackPoster slackPoster;

private enum NotificationType {
creation, approval
Expand All @@ -63,7 +68,8 @@ private enum NotificationType {
RoleServices roleServices,
MemberProfileServices memberProfileServices,
@Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender,
CheckInsConfiguration checkInsConfiguration) {
CheckInsConfiguration checkInsConfiguration,
SlackPoster slackPoster) {
this.kudosRepository = kudosRepository;
this.kudosRecipientServices = kudosRecipientServices;
this.kudosRecipientRepository = kudosRecipientRepository;
Expand All @@ -74,6 +80,7 @@ private enum NotificationType {
this.currentUserServices = currentUserServices;
this.emailSender = emailSender;
this.checkInsConfiguration = checkInsConfiguration;
this.slackPoster = slackPoster;
}

@Override
Expand Down Expand Up @@ -341,6 +348,7 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
recipientAddresses.add(member.getWorkEmail());
}
}
slackApprovedKudos(kudos);
break;
case NotificationType.creation:
content = getAdminEmailContent(checkInsConfiguration);
Expand All @@ -366,4 +374,16 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
LOG.error("An unexpected error occurred while sending notifications: {}", ex.getLocalizedMessage(), ex);
}
}

private void slackApprovedKudos(Kudos kudos) {
KudosConverter converter = new KudosConverter(memberProfileServices,
kudosRecipientServices);

String slackBlock = converter.toSlackBlock(kudos);
HttpResponse httpResponse =
slackPoster.post(converter.toSlackBlock(kudos));
if (httpResponse.status() != HttpStatus.OK) {
LOG.error("Unable to POST to Slack: " + httpResponse.reason());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.objectcomputing.checkins.services;

import com.objectcomputing.checkins.notifications.social_media.SlackPoster;

import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;

import jakarta.inject.Singleton;

import java.util.List;
import java.util.ArrayList;

@Singleton
@Replaces(SlackPoster.class)
@Requires(property = "replace.slackposter", value = StringUtils.TRUE)
public class SlackPosterReplacement extends SlackPoster {
public final List<String> posted = new ArrayList<>();

public void reset() {
posted.clear();
}

public HttpResponse post(String slackBlock) {
posted.add(slackBlock);
return HttpResponse.status(HttpStatus.OK);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ default LocalDate getRandomLocalDateTime(LocalDateTime start, LocalDateTime end)
LocalDate startDate = start.toLocalDate();
long daysBetween = ChronoUnit.DAYS.between(startDate, end.toLocalDate());
Random random = new Random();
long randomDays = random.nextLong(daysBetween);
long randomDays = daysBetween > 0 ? random.nextLong(daysBetween) : 0;

return startDate.plusDays(randomDays);
}
Expand Down
Loading
Loading