Skip to content

Commit e2d3243

Browse files
authored
Merge branch 'develop' into feature-2534/additional-test-mockito-removal
2 parents 17f9706 + 7151f2d commit e2d3243

File tree

14 files changed

+386
-3
lines changed

14 files changed

+386
-3
lines changed

.github/workflows/gradle-build-production.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ jobs:
8585
--set-env-vars "[email protected]" \
8686
--set-env-vars "FROM_NAME=Check-Ins" \
8787
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=cloud,google,gcp" \
88+
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
89+
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
8890
--platform "managed" \
8991
--max-instances 8 \
9092
--allow-unauthenticated

.github/workflows/gradle-deploy-develop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ jobs:
110110
--set-env-vars "[email protected]" \
111111
--set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \
112112
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
113+
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
114+
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
113115
--platform "managed" \
114116
--max-instances 2 \
115117
--allow-unauthenticated

.github/workflows/gradle-deploy-native-develop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ jobs:
109109
--set-env-vars "[email protected]" \
110110
--set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \
111111
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
112+
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
113+
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
112114
--platform "managed" \
113115
--max-instances 2 \
114116
--allow-unauthenticated

server/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ dependencies {
117117
implementation("io.micrometer:context-propagation")
118118

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

121122
testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
122123
testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion"

server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public static class ApplicationConfig {
3131
@NotNull
3232
private GoogleApiConfig googleApi;
3333

34+
@NotNull
35+
private NotificationsConfig notifications;
36+
3437
@Getter
3538
@Setter
3639
@ConfigurationProperties("feedback")
@@ -66,5 +69,25 @@ public static class ScopeConfig {
6669
private String scopeForDirectoryApi;
6770
}
6871
}
72+
73+
@Getter
74+
@Setter
75+
@ConfigurationProperties("notifications")
76+
public static class NotificationsConfig {
77+
78+
@NotNull
79+
private SlackConfig slack;
80+
81+
@Getter
82+
@Setter
83+
@ConfigurationProperties("slack")
84+
public static class SlackConfig {
85+
@NotBlank
86+
private String webhookUrl;
87+
88+
@NotBlank
89+
private String botToken;
90+
}
91+
}
6992
}
7093
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.objectcomputing.checkins.notifications.social_media;
2+
3+
import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
4+
import io.micronaut.http.HttpRequest;
5+
import io.micronaut.http.HttpResponse;
6+
import io.micronaut.http.HttpStatus;
7+
import io.micronaut.http.client.BlockingHttpClient;
8+
import io.micronaut.http.client.HttpClient;
9+
10+
import jakarta.inject.Singleton;
11+
import jakarta.inject.Inject;
12+
13+
import java.util.List;
14+
15+
@Singleton
16+
public class SlackPoster {
17+
@Inject
18+
private HttpClient slackClient;
19+
20+
@Inject
21+
private CheckInsConfiguration configuration;
22+
23+
public HttpResponse post(String slackBlock) {
24+
// See if we can have a webhook URL.
25+
String slackWebHook = configuration.getApplication().getNotifications().getSlack().getWebhookUrl();
26+
if (slackWebHook != null) {
27+
// POST it to Slack.
28+
BlockingHttpClient client = slackClient.toBlocking();
29+
HttpRequest<String> request = HttpRequest.POST(slackWebHook,
30+
slackBlock);
31+
return client.exchange(request);
32+
}
33+
return HttpResponse.status(HttpStatus.GONE,
34+
"Slack Webhook URL is not configured");
35+
}
36+
}
37+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.objectcomputing.checkins.notifications.social_media;
2+
3+
import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
4+
import com.slack.api.model.block.LayoutBlock;
5+
import com.slack.api.Slack;
6+
import com.slack.api.methods.MethodsClient;
7+
import com.slack.api.model.Conversation;
8+
import com.slack.api.methods.SlackApiException;
9+
import com.slack.api.methods.request.conversations.ConversationsListRequest;
10+
import com.slack.api.methods.response.conversations.ConversationsListResponse;
11+
import com.slack.api.methods.request.users.UsersLookupByEmailRequest;
12+
import com.slack.api.methods.response.users.UsersLookupByEmailResponse;
13+
14+
import jakarta.inject.Singleton;
15+
import jakarta.inject.Inject;
16+
17+
import java.util.List;
18+
import java.io.IOException;
19+
20+
import jnr.ffi.annotations.In;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
@Singleton
25+
public class SlackSearch {
26+
private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class);
27+
28+
private CheckInsConfiguration configuration;
29+
30+
public SlackSearch(CheckInsConfiguration checkInsConfiguration) {
31+
this.configuration = checkInsConfiguration;
32+
}
33+
34+
public String findChannelId(String channelName) {
35+
String token = configuration.getApplication().getNotifications().getSlack().getBotToken();
36+
if (token != null) {
37+
try {
38+
MethodsClient client = Slack.getInstance().methods(token);
39+
ConversationsListResponse response = client.conversationsList(
40+
ConversationsListRequest.builder().build()
41+
);
42+
43+
if (response.isOk()) {
44+
for (Conversation conversation: response.getChannels()) {
45+
if (conversation.getName().equals(channelName)) {
46+
return conversation.getId();
47+
}
48+
}
49+
}
50+
} catch(IOException e) {
51+
LOG.error("SlackSearch.findChannelId: " + e.toString());
52+
} catch(SlackApiException e) {
53+
LOG.error("SlackSearch.findChannelId: " + e.toString());
54+
}
55+
}
56+
return null;
57+
}
58+
59+
public String findUserId(String userEmail) {
60+
String token = configuration.getApplication().getNotifications().getSlack().getBotToken();
61+
if (token != null) {
62+
try {
63+
MethodsClient client = Slack.getInstance().methods(token);
64+
UsersLookupByEmailResponse response = client.usersLookupByEmail(
65+
UsersLookupByEmailRequest.builder().email(userEmail).build()
66+
);
67+
68+
if (response.isOk()) {
69+
return response.getUser().getId();
70+
}
71+
} catch(IOException e) {
72+
LOG.error("SlackSearch.findUserId: " + e.toString());
73+
} catch(SlackApiException e) {
74+
LOG.error("SlackSearch.findUserId: " + e.toString());
75+
}
76+
}
77+
return null;
78+
}
79+
}
80+
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.objectcomputing.checkins.services.kudos;
2+
3+
import com.objectcomputing.checkins.notifications.social_media.SlackSearch;
4+
import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServices;
5+
import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient;
6+
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
7+
import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils;
8+
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
9+
10+
import com.slack.api.model.block.LayoutBlock;
11+
import com.slack.api.model.block.RichTextBlock;
12+
import com.slack.api.model.block.element.RichTextElement;
13+
import com.slack.api.model.block.element.RichTextSectionElement;
14+
import com.slack.api.util.json.GsonFactory;
15+
import com.google.gson.Gson;
16+
17+
import jakarta.inject.Singleton;
18+
19+
import java.util.UUID;
20+
import java.util.List;
21+
import java.util.ArrayList;
22+
23+
@Singleton
24+
public class KudosConverter {
25+
private record InternalBlock(
26+
List<LayoutBlock> blocks
27+
) {}
28+
29+
private final MemberProfileServices memberProfileServices;
30+
private final KudosRecipientServices kudosRecipientServices;
31+
private final SlackSearch slackSearch;
32+
33+
public KudosConverter(MemberProfileServices memberProfileServices,
34+
KudosRecipientServices kudosRecipientServices,
35+
SlackSearch slackSearch) {
36+
this.memberProfileServices = memberProfileServices;
37+
this.kudosRecipientServices = kudosRecipientServices;
38+
this.slackSearch = slackSearch;
39+
}
40+
41+
public String toSlackBlock(Kudos kudos) {
42+
// Build the message text out of the Kudos data.
43+
List<RichTextElement> content = new ArrayList<>();
44+
content.add(
45+
RichTextSectionElement.Text.builder()
46+
.text("Kudos from ")
47+
.style(boldItalic())
48+
.build()
49+
);
50+
content.add(memberAsRichText(kudos.getSenderId()));
51+
content.addAll(recipients(kudos));
52+
53+
content.add(
54+
RichTextSectionElement.Text.builder()
55+
.text("\n" + kudos.getMessage() + "\n")
56+
.style(boldItalic())
57+
.build()
58+
);
59+
60+
// Bring it all together.
61+
RichTextSectionElement element = RichTextSectionElement.builder()
62+
.elements(content).build();
63+
RichTextBlock richTextBlock = RichTextBlock.builder()
64+
.elements(List.of(element)).build();
65+
InternalBlock block = new InternalBlock(List.of(richTextBlock));
66+
Gson mapper = GsonFactory.createSnakeCase();
67+
return mapper.toJson(block);
68+
}
69+
70+
private RichTextSectionElement.TextStyle boldItalic() {
71+
return RichTextSectionElement.TextStyle.builder()
72+
.bold(true).italic(true).build();
73+
}
74+
75+
private RichTextSectionElement.LimitedTextStyle limitedBoldItalic() {
76+
return RichTextSectionElement.LimitedTextStyle.builder()
77+
.bold(true).italic(true).build();
78+
}
79+
80+
private RichTextElement memberAsRichText(UUID memberId) {
81+
// Look up the user id by email address on Slack
82+
MemberProfile profile = memberProfileServices.getById(memberId);
83+
String userId = slackSearch.findUserId(profile.getWorkEmail());
84+
if (userId == null) {
85+
String name = MemberProfileUtils.getFullName(profile);
86+
return RichTextSectionElement.Text.builder()
87+
.text("@" + name)
88+
.style(boldItalic())
89+
.build();
90+
} else {
91+
return RichTextSectionElement.User.builder()
92+
.userId(userId)
93+
.style(limitedBoldItalic())
94+
.build();
95+
}
96+
}
97+
98+
private List<RichTextElement> recipients(Kudos kudos) {
99+
List<RichTextElement> list = new ArrayList<>();
100+
List<KudosRecipient> recipients =
101+
kudosRecipientServices.getAllByKudosId(kudos.getId());
102+
String separator = " to ";
103+
for (KudosRecipient recipient : recipients) {
104+
list.add(RichTextSectionElement.Text.builder()
105+
.text(separator)
106+
.style(boldItalic())
107+
.build());
108+
list.add(memberAsRichText(recipient.getMemberId()));
109+
separator = ", ";
110+
}
111+
return list;
112+
}
113+
}
114+

server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
44
import com.objectcomputing.checkins.notifications.email.EmailSender;
55
import com.objectcomputing.checkins.notifications.email.MailJetFactory;
6+
import com.objectcomputing.checkins.notifications.social_media.SlackPoster;
67
import com.objectcomputing.checkins.exceptions.BadArgException;
78
import com.objectcomputing.checkins.exceptions.NotFoundException;
89
import com.objectcomputing.checkins.exceptions.PermissionException;
@@ -21,6 +22,9 @@
2122
import com.objectcomputing.checkins.util.Util;
2223
import io.micronaut.core.annotation.Nullable;
2324
import io.micronaut.transaction.annotation.Transactional;
25+
import io.micronaut.http.HttpResponse;
26+
import io.micronaut.http.HttpStatus;
27+
2428
import jakarta.inject.Named;
2529
import jakarta.inject.Singleton;
2630
import org.slf4j.Logger;
@@ -49,6 +53,8 @@ class KudosServicesImpl implements KudosServices {
4953
private final CheckInsConfiguration checkInsConfiguration;
5054
private final RoleServices roleServices;
5155
private final MemberProfileServices memberProfileServices;
56+
private final SlackPoster slackPoster;
57+
private final KudosConverter converter;
5258

5359
private enum NotificationType {
5460
creation, approval
@@ -63,7 +69,10 @@ private enum NotificationType {
6369
RoleServices roleServices,
6470
MemberProfileServices memberProfileServices,
6571
@Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender,
66-
CheckInsConfiguration checkInsConfiguration) {
72+
CheckInsConfiguration checkInsConfiguration,
73+
SlackPoster slackPoster,
74+
KudosConverter converter
75+
) {
6776
this.kudosRepository = kudosRepository;
6877
this.kudosRecipientServices = kudosRecipientServices;
6978
this.kudosRecipientRepository = kudosRecipientRepository;
@@ -74,6 +83,8 @@ private enum NotificationType {
7483
this.currentUserServices = currentUserServices;
7584
this.emailSender = emailSender;
7685
this.checkInsConfiguration = checkInsConfiguration;
86+
this.slackPoster = slackPoster;
87+
this.converter = converter;
7788
}
7889

7990
@Override
@@ -341,6 +352,7 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
341352
recipientAddresses.add(member.getWorkEmail());
342353
}
343354
}
355+
slackApprovedKudos(kudos);
344356
break;
345357
case NotificationType.creation:
346358
content = getAdminEmailContent(checkInsConfiguration);
@@ -366,4 +378,12 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
366378
LOG.error("An unexpected error occurred while sending notifications: {}", ex.getLocalizedMessage(), ex);
367379
}
368380
}
381+
382+
private void slackApprovedKudos(Kudos kudos) {
383+
HttpResponse httpResponse =
384+
slackPoster.post(converter.toSlackBlock(kudos));
385+
if (httpResponse.status() != HttpStatus.OK) {
386+
LOG.error("Unable to POST to Slack: " + httpResponse.reason());
387+
}
388+
}
369389
}

server/src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ check-ins:
9797
feedback:
9898
max-suggestions: 6
9999
request-subject: "Feedback request"
100+
notifications:
101+
slack:
102+
webhook-url: ${ SLACK_WEBHOOK_URL }
103+
bot-token: ${ SLACK_BOT_TOKEN }
100104
web-address: ${ WEB_ADDRESS }
101105
---
102106
flyway:

0 commit comments

Comments
 (0)