Skip to content

Commit 219c330

Browse files
committed
Send an approved public kudos to the #kudos slack channel.
1 parent a41349f commit 219c330

File tree

5 files changed

+258
-1
lines changed

5 files changed

+258
-1
lines changed

server/build.gradle

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

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

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

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,7 @@ class KudosServicesImpl implements KudosServices {
4953
private final CheckInsConfiguration checkInsConfiguration;
5054
private final RoleServices roleServices;
5155
private final MemberProfileServices memberProfileServices;
56+
private final SlackPoster slackPoster;
5257

5358
private enum NotificationType {
5459
creation, approval
@@ -63,7 +68,8 @@ private enum NotificationType {
6368
RoleServices roleServices,
6469
MemberProfileServices memberProfileServices,
6570
@Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender,
66-
CheckInsConfiguration checkInsConfiguration) {
71+
CheckInsConfiguration checkInsConfiguration,
72+
SlackPoster slackPoster) {
6773
this.kudosRepository = kudosRepository;
6874
this.kudosRecipientServices = kudosRecipientServices;
6975
this.kudosRecipientRepository = kudosRecipientRepository;
@@ -74,6 +80,7 @@ private enum NotificationType {
7480
this.currentUserServices = currentUserServices;
7581
this.emailSender = emailSender;
7682
this.checkInsConfiguration = checkInsConfiguration;
83+
this.slackPoster = slackPoster;
7784
}
7885

7986
@Override
@@ -341,6 +348,7 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
341348
recipientAddresses.add(member.getWorkEmail());
342349
}
343350
}
351+
slackApprovedKudos(kudos);
344352
break;
345353
case NotificationType.creation:
346354
content = getAdminEmailContent(checkInsConfiguration);
@@ -366,4 +374,16 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) {
366374
LOG.error("An unexpected error occurred while sending notifications: {}", ex.getLocalizedMessage(), ex);
367375
}
368376
}
377+
378+
private void slackApprovedKudos(Kudos kudos) {
379+
KudosConverter converter = new KudosConverter(memberProfileServices,
380+
kudosRecipientServices);
381+
382+
String slackBlock = converter.toSlackBlock(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
}

0 commit comments

Comments
 (0)