From 219c3304b7f8954ba56b01f4c91a2e12bea9a92a Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 12:13:02 -0600 Subject: [PATCH 01/10] Send an approved public kudos to the #kudos slack channel. --- server/build.gradle | 1 + .../social_media/SlackPoster.java | 33 +++++ .../social_media/SlackSearch.java | 73 ++++++++++ .../services/kudos/KudosConverter.java | 130 ++++++++++++++++++ .../services/kudos/KudosServicesImpl.java | 22 ++- 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java diff --git a/server/build.gradle b/server/build.gradle index 54a3b0f28b..8d680f9dd3 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -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" diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java new file mode 100644 index 0000000000..0d561b2f0f --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java @@ -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 request = HttpRequest.POST(slackWebHook, + slackBlock); + return client.exchange(request); + } + return HttpResponse.status(HttpStatus.GONE, + "Slack Webhook URL is not configured"); + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java new file mode 100644 index 0000000000..58ae30c75a --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -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; + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java new file mode 100644 index 0000000000..460dddabdb --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -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 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 some the message text out of the Kudos data. + List 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 name to get the user id from 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 recipients(Kudos kudos) { + List list = new ArrayList<>(); + List 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; + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index c672dbd439..8d429b8aa1 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -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; @@ -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; @@ -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 @@ -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; @@ -74,6 +80,7 @@ private enum NotificationType { this.currentUserServices = currentUserServices; this.emailSender = emailSender; this.checkInsConfiguration = checkInsConfiguration; + this.slackPoster = slackPoster; } @Override @@ -341,6 +348,7 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { recipientAddresses.add(member.getWorkEmail()); } } + slackApprovedKudos(kudos); break; case NotificationType.creation: content = getAdminEmailContent(checkInsConfiguration); @@ -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()); + } + } } From bce7706035e5fb6160edf28129a67c3cc771fe74 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 13:15:16 -0600 Subject: [PATCH 02/10] Disable this test in native due to reflexive nature of Gson and records. --- .../checkins/services/kudos/KudosControllerTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 92975598a4..6dbe8400c0 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.time.LocalDate; import java.util.Collections; @@ -46,6 +47,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +// Disabled in nativetest due to a ReflectiveOperationException from Gson +// when attempting to post public Kudos to Slack. +@DisabledInNativeImage @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) class KudosControllerTest extends TestContainersSuite implements KudosFixture, TeamFixture, RoleFixture { @Inject From 59ec1159e4428a913362bb92e5851ebf799f28fc Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 15 Nov 2024 13:17:29 -0600 Subject: [PATCH 03/10] Handle situation where daysBetween is zero. --- .../checkins/services/fixture/FeedbackRequestFixture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java index 201a67023e..f3aec2d02b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java @@ -62,7 +62,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); } From 917e22f6dd8a571865599c097769f9bb63fb4867 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 18 Nov 2024 08:49:27 -0600 Subject: [PATCH 04/10] Inspect the posted slack block during kudos approval. --- .../services/SlackPosterReplacement.java | 31 ++++++++++ .../services/kudos/KudosControllerTest.java | 62 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java new file mode 100644 index 0000000000..c4283cc651 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackPosterReplacement.java @@ -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 posted = new ArrayList<>(); + + public void reset() { + posted.clear(); + } + + public HttpResponse post(String slackBlock) { + posted.add(slackBlock); + return HttpResponse.status(HttpStatus.OK); + } +} + diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 6dbe8400c0..94f9756fca 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -3,12 +3,14 @@ import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; +import com.objectcomputing.checkins.services.SlackPosterReplacement; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.fixture.KudosFixture; import com.objectcomputing.checkins.services.fixture.TeamFixture; import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServicesImpl; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.team.Team; import io.micronaut.core.type.Argument; @@ -30,9 +32,16 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.api.condition.DisabledInNativeImage; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.core.JsonProcessingException; + import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -51,11 +60,15 @@ // when attempting to post public Kudos to Slack. @DisabledInNativeImage @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) +@Property(name = "replace.slackposter", value = StringUtils.TRUE) class KudosControllerTest extends TestContainersSuite implements KudosFixture, TeamFixture, RoleFixture { @Inject @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; + @Inject + private SlackPosterReplacement slackPoster; + @Inject @Client("/services/kudos") HttpClient httpClient; @@ -93,6 +106,7 @@ void setUp() { message = "Kudos!"; emailSender.reset(); + slackPoster.reset(); } @ParameterizedTest @@ -210,7 +224,7 @@ void testCreateKudosWithEmptyRecipientMembers() { } @Test - void testApproveKudos() { + void testApproveKudos() throws JsonProcessingException { Kudos kudos = createPublicKudos(senderId); assertNull(kudos.getDateApproved()); KudosRecipient recipient = createKudosRecipient(kudos.getId(), recipientMembers.getFirst().getId()); @@ -231,6 +245,52 @@ void testApproveKudos() { ), emailSender.events.getFirst() ); + + // Check the posted slack block + assertEquals(1, slackPoster.posted.size()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode posted = mapper.readTree(slackPoster.posted.get(0)); + + assertEquals(JsonNodeType.OBJECT, posted.getNodeType()); + JsonNode blocks = posted.get("blocks"); + assertEquals(JsonNodeType.ARRAY, blocks.getNodeType()); + + var iter = blocks.elements(); + assertTrue(iter.hasNext()); + JsonNode block = iter.next(); + + assertEquals(JsonNodeType.OBJECT, block.getNodeType()); + JsonNode elements = block.get("elements"); + assertEquals(JsonNodeType.ARRAY, elements.getNodeType()); + + iter = elements.elements(); + assertTrue(iter.hasNext()); + JsonNode element = iter.next(); + + assertEquals(JsonNodeType.OBJECT, element.getNodeType()); + JsonNode innerElements = element.get("elements"); + assertEquals(JsonNodeType.ARRAY, innerElements.getNodeType()); + + iter = innerElements.elements(); + assertTrue(iter.hasNext()); + + // The real SlackPoster will look up user ids in Slack and use those in + // the posted message. Failing the lookup, it will use @. + String from = "@" + MemberProfileUtils.getFullName(sender); + String to = "@" + MemberProfileUtils.getFullName(recipientMembers.get(0)); + boolean foundFrom = false; + boolean foundTo = false; + while(iter.hasNext()) { + element = iter.next(); + assertEquals(JsonNodeType.OBJECT, element.getNodeType()); + String value = element.get("text").asText(); + if (value.equals(from)) { + foundFrom = true; + } else if (value.equals(to)) { + foundTo = true; + } + } + assertTrue(foundFrom && foundTo); } @Test From c9805297bd83f7a49b185ec3fba5bd93bf4d51be Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 18 Nov 2024 10:09:34 -0600 Subject: [PATCH 05/10] Non-functional cleanup. --- .../checkins/services/kudos/KudosConverter.java | 4 ++-- .../checkins/services/kudos/KudosControllerTest.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index 460dddabdb..ee3ff465f5 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -34,7 +34,7 @@ public KudosConverter(MemberProfileServices memberProfileServices, } public String toSlackBlock(Kudos kudos) { - // Build some the message text out of the Kudos data. + // Build the message text out of the Kudos data. List content = new ArrayList<>(); // Look up the channel id from Slack @@ -93,7 +93,7 @@ private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() { } private RichTextElement memberAsRichText(UUID memberId) { - // Look up the user name to get the user id from Slack + // 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()); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 94f9756fca..e166954846 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -41,7 +41,6 @@ import java.time.LocalDate; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; From 219d89c641c31250430792158a8e4c0d8652fac8 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 15:58:37 -0600 Subject: [PATCH 06/10] Adjusted the configuration of the slack integration --- .../configuration/CheckInsConfiguration.java | 23 +++++++++++++++++++ .../social_media/SlackPoster.java | 6 ++++- .../social_media/SlackSearch.java | 7 +++++- server/src/main/resources/application.yml | 4 ++++ .../src/test/resources/application-test.yml | 4 ++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java index b1c707a559..6c58600b40 100644 --- a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java +++ b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java @@ -31,6 +31,9 @@ public static class ApplicationConfig { @NotNull private GoogleApiConfig googleApi; + @NotNull + private NotificationsConfig notifications; + @Getter @Setter @ConfigurationProperties("feedback") @@ -66,5 +69,25 @@ public static class ScopeConfig { private String scopeForDirectoryApi; } } + + @Getter + @Setter + @ConfigurationProperties("notifications") + public static class NotificationsConfig { + + @NotNull + private SlackConfig slack; + + @Getter + @Setter + @ConfigurationProperties("slack") + public static class SlackConfig { + @NotBlank + private String webhookUrl; + + @NotBlank + private String botToken; + } + } } } diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java index 0d561b2f0f..be08f13e14 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackPoster.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.notifications.social_media; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; @@ -16,9 +17,12 @@ public class SlackPoster { @Inject private HttpClient slackClient; + @Inject + private CheckInsConfiguration configuration; + public HttpResponse post(String slackBlock) { // See if we can have a webhook URL. - String slackWebHook = System.getenv("SLACK_WEBHOOK_URL"); + String slackWebHook = configuration.getApplication().getNotifications().getSlack().getWebhookUrl(); if (slackWebHook != null) { // POST it to Slack. BlockingHttpClient client = slackClient.toBlocking(); diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index 58ae30c75a..ea1494a8e9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.notifications.social_media; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.slack.api.model.block.LayoutBlock; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; @@ -16,6 +17,7 @@ import java.util.List; import java.io.IOException; +import jnr.ffi.annotations.In; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,8 +26,11 @@ public class SlackSearch { private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class); private static final String env = "SLACK_BOT_TOKEN"; + @Inject + private CheckInsConfiguration configuration; + public String findChannelId(String channelName) { - String token = System.getenv(env); + String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { try { MethodsClient client = Slack.getInstance().methods(token); diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index fd8c2ea7f9..99148dcede 100755 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -97,6 +97,10 @@ check-ins: feedback: max-suggestions: 6 request-subject: "Feedback request" + notifications: + slack: + webhook-url: ${ SLACK_WEBHOOK_URL } + bot-token: ${ SLACK_BOT_TOKEN } web-address: ${ WEB_ADDRESS } --- flyway: diff --git a/server/src/test/resources/application-test.yml b/server/src/test/resources/application-test.yml index 5ffff78794..c6fca9bd81 100644 --- a/server/src/test/resources/application-test.yml +++ b/server/src/test/resources/application-test.yml @@ -41,6 +41,10 @@ check-ins: application: google-api: delegated-user: test@objectcomputing.com + notifications: + slack: + webhook-url: https://bogus.objectcomputing.com/slack + bot-token: BOGUS_TOKEN --- aes: key: BOGUS_TEST_KEY From b48fab595bbec3da9d76b61e9d86ca388fb3cd0d Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 16:02:49 -0600 Subject: [PATCH 07/10] Fixed missed configuration point --- .../checkins/notifications/social_media/SlackSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index ea1494a8e9..8174e98052 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -55,7 +55,7 @@ public String findChannelId(String channelName) { } public String findUserId(String userEmail) { - String token = System.getenv(env); + String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { try { MethodsClient client = Slack.getInstance().methods(token); From 0040daa8e3eb48490604aad776f36a76a21dafaf Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Thu, 2 Jan 2025 16:50:07 -0600 Subject: [PATCH 08/10] Injected the KudosConverter and the SlackSearch --- .../notifications/social_media/SlackSearch.java | 6 ++++-- .../checkins/services/kudos/KudosConverter.java | 14 +++++++++----- .../checkins/services/kudos/KudosServicesImpl.java | 10 +++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index 8174e98052..526fdf6a05 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -24,11 +24,13 @@ @Singleton public class SlackSearch { private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class); - private static final String env = "SLACK_BOT_TOKEN"; - @Inject private CheckInsConfiguration configuration; + public SlackSearch(CheckInsConfiguration checkInsConfiguration) { + this.configuration = checkInsConfiguration; + } + public String findChannelId(String channelName) { String token = configuration.getApplication().getNotifications().getSlack().getBotToken(); if (token != null) { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index ee3ff465f5..6742ef692b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -14,11 +14,14 @@ import com.slack.api.model.block.element.RichTextSectionElement; import com.slack.api.util.json.GsonFactory; import com.google.gson.Gson; +import io.micronaut.core.annotation.Introspected; +import jakarta.inject.Singleton; import java.util.UUID; import java.util.List; import java.util.ArrayList; +@Singleton public class KudosConverter { private record InternalBlock( List blocks @@ -26,11 +29,14 @@ private record InternalBlock( private final MemberProfileServices memberProfileServices; private final KudosRecipientServices kudosRecipientServices; + private final SlackSearch slackSearch; public KudosConverter(MemberProfileServices memberProfileServices, - KudosRecipientServices kudosRecipientServices) { + KudosRecipientServices kudosRecipientServices, + SlackSearch slackSearch) { this.memberProfileServices = memberProfileServices; this.kudosRecipientServices = kudosRecipientServices; + this.slackSearch = slackSearch; } public String toSlackBlock(Kudos kudos) { @@ -39,8 +45,7 @@ public String toSlackBlock(Kudos kudos) { // Look up the channel id from Slack String channelName = "kudos"; - SlackSearch search = new SlackSearch(); - String channelId = search.findChannelId(channelName); + String channelId = slackSearch.findChannelId(channelName); if (channelId == null) { content.add( RichTextSectionElement.Text.builder() @@ -94,9 +99,8 @@ private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() { 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()); + String userId = slackSearch.findUserId(profile.getWorkEmail()); if (userId == null) { String name = MemberProfileUtils.getFullName(profile); return RichTextSectionElement.Text.builder() diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 8d429b8aa1..64750cbba0 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -54,6 +54,7 @@ class KudosServicesImpl implements KudosServices { private final RoleServices roleServices; private final MemberProfileServices memberProfileServices; private final SlackPoster slackPoster; + private final KudosConverter converter; private enum NotificationType { creation, approval @@ -69,7 +70,9 @@ private enum NotificationType { MemberProfileServices memberProfileServices, @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, CheckInsConfiguration checkInsConfiguration, - SlackPoster slackPoster) { + SlackPoster slackPoster, + KudosConverter converter + ) { this.kudosRepository = kudosRepository; this.kudosRecipientServices = kudosRecipientServices; this.kudosRecipientRepository = kudosRecipientRepository; @@ -81,6 +84,7 @@ private enum NotificationType { this.emailSender = emailSender; this.checkInsConfiguration = checkInsConfiguration; this.slackPoster = slackPoster; + this.converter = converter; } @Override @@ -376,10 +380,6 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { } 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) { From 40f63181a3d01079bf604bca3a7bbe7a49c6c0c5 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Tue, 7 Jan 2025 14:13:48 -0600 Subject: [PATCH 09/10] Removed channel reference from the slack posting --- .../services/kudos/KudosConverter.java | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index 6742ef692b..798cae7974 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -1,6 +1,5 @@ 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; @@ -14,7 +13,7 @@ import com.slack.api.model.block.element.RichTextSectionElement; import com.slack.api.util.json.GsonFactory; import com.google.gson.Gson; -import io.micronaut.core.annotation.Introspected; + import jakarta.inject.Singleton; import java.util.UUID; @@ -42,28 +41,9 @@ public KudosConverter(MemberProfileServices memberProfileServices, public String toSlackBlock(Kudos kudos) { // Build the message text out of the Kudos data. List content = new ArrayList<>(); - - // Look up the channel id from Slack - String channelName = "kudos"; - String channelId = slackSearch.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 ") + .text("Kudos from ") .style(boldItalic()) .build() ); From 50688d06ac1df42b3eaf4dc3a21508e0e462e85d Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Tue, 7 Jan 2025 14:20:57 -0600 Subject: [PATCH 10/10] Added slack parameters to deployment workflows --- .github/workflows/gradle-build-production.yml | 2 ++ .github/workflows/gradle-deploy-develop.yml | 2 ++ .github/workflows/gradle-deploy-native-develop.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/gradle-build-production.yml b/.github/workflows/gradle-build-production.yml index 6d21be4b68..64ab7c4302 100644 --- a/.github/workflows/gradle-build-production.yml +++ b/.github/workflows/gradle-build-production.yml @@ -85,6 +85,8 @@ jobs: --set-env-vars "FROM_ADDRESS=no-reply@objectcomputing.com" \ --set-env-vars "FROM_NAME=Check-Ins" \ --set-env-vars "^@^MICRONAUT_ENVIRONMENTS=cloud,google,gcp" \ + --set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \ + --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --platform "managed" \ --max-instances 8 \ --allow-unauthenticated diff --git a/.github/workflows/gradle-deploy-develop.yml b/.github/workflows/gradle-deploy-develop.yml index 2a17c12cbf..ba2dddfc47 100644 --- a/.github/workflows/gradle-deploy-develop.yml +++ b/.github/workflows/gradle-deploy-develop.yml @@ -110,6 +110,8 @@ jobs: --set-env-vars "FROM_ADDRESS=no-reply@objectcomputing.com" \ --set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \ --set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \ + --set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \ + --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --platform "managed" \ --max-instances 2 \ --allow-unauthenticated diff --git a/.github/workflows/gradle-deploy-native-develop.yml b/.github/workflows/gradle-deploy-native-develop.yml index c82cc23bf7..cb40d97f93 100644 --- a/.github/workflows/gradle-deploy-native-develop.yml +++ b/.github/workflows/gradle-deploy-native-develop.yml @@ -109,6 +109,8 @@ jobs: --set-env-vars "FROM_ADDRESS=kimberlinm@objectcomputing.com" \ --set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \ --set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \ + --set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \ + --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --platform "managed" \ --max-instances 2 \ --allow-unauthenticated