Skip to content

Commit

Permalink
Merge branch 'develop' into feature-2801/pulse-page-adjustments
Browse files Browse the repository at this point in the history
  • Loading branch information
ocielliottc authored Jan 8, 2025
2 parents b4a0a63 + 7151f2d commit dbcc427
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 3 deletions.
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

0 comments on commit dbcc427

Please sign in to comment.