Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
219c330
Send an approved public kudos to the #kudos slack channel.
ocielliottc Nov 15, 2024
26fe554
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
ocielliottc Nov 15, 2024
bce7706
Disable this test in native due to reflexive nature of Gson and records.
ocielliottc Nov 15, 2024
59ec115
Handle situation where daysBetween is zero.
ocielliottc Nov 15, 2024
917e22f
Inspect the posted slack block during kudos approval.
ocielliottc Nov 18, 2024
c980529
Non-functional cleanup.
ocielliottc Nov 18, 2024
74c159d
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Nov 21, 2024
d4bcf53
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Nov 21, 2024
9e3414d
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Nov 25, 2024
94c6d41
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Dec 11, 2024
52643a9
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Dec 17, 2024
35143ef
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Dec 18, 2024
fa6db81
Merge branch 'develop' into feature-2751/post-approved-kudos-to-slack
mkimberlin Jan 2, 2025
219d89c
Adjusted the configuration of the slack integration
mkimberlin Jan 2, 2025
b48fab5
Fixed missed configuration point
mkimberlin Jan 2, 2025
0040daa
Injected the KudosConverter and the SlackSearch
mkimberlin Jan 2, 2025
40f6318
Removed channel reference from the slack posting
mkimberlin Jan 7, 2025
50688d0
Added slack parameters to deployment workflows
mkimberlin Jan 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/gradle-build-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-native-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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
Expand Up @@ -31,6 +31,9 @@ public static class ApplicationConfig {
@NotNull
private GoogleApiConfig googleApi;

@NotNull
private NotificationsConfig notifications;

@Getter
@Setter
@ConfigurationProperties("feedback")
Expand Down Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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;
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;

@Inject
private CheckInsConfiguration configuration;

public HttpResponse post(String slackBlock) {
// See if we can have a webhook URL.
String slackWebHook = configuration.getApplication().getNotifications().getSlack().getWebhookUrl();
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,80 @@
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;
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 jnr.ffi.annotations.In;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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) {
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 = configuration.getApplication().getNotifications().getSlack().getBotToken();
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,114 @@
package com.objectcomputing.checkins.services.kudos;

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 jakarta.inject.Singleton;

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

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

private final MemberProfileServices memberProfileServices;
private final KudosRecipientServices kudosRecipientServices;
private final SlackSearch slackSearch;

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

public String toSlackBlock(Kudos kudos) {
// Build the message text out of the Kudos data.
List<RichTextElement> content = new ArrayList<>();
content.add(
RichTextSectionElement.Text.builder()
.text("Kudos 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
MemberProfile profile = memberProfileServices.getById(memberId);
String userId = slackSearch.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,8 @@ class KudosServicesImpl implements KudosServices {
private final CheckInsConfiguration checkInsConfiguration;
private final RoleServices roleServices;
private final MemberProfileServices memberProfileServices;
private final SlackPoster slackPoster;
private final KudosConverter converter;

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

@Override
Expand Down Expand Up @@ -341,6 +352,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 +378,12 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
LOG.error("An unexpected error occurred while sending notifications: {}", ex.getLocalizedMessage(), ex);
}
}

private void slackApprovedKudos(Kudos kudos) {
HttpResponse httpResponse =
slackPoster.post(converter.toSlackBlock(kudos));
if (httpResponse.status() != HttpStatus.OK) {
LOG.error("Unable to POST to Slack: " + httpResponse.reason());
}
}
}
4 changes: 4 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading