Skip to content

Commit b59208f

Browse files
authored
Merge pull request #2820 from objectcomputing/feature-2814/slack-pulse-survey-integration
Feature 2814/slack pulse survey integration
2 parents 36175c6 + 931663e commit b59208f

File tree

21 files changed

+628
-31
lines changed

21 files changed

+628
-31
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ jobs:
8787
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=cloud,google,gcp" \
8888
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
8989
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
90+
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
91+
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
9092
--platform "managed" \
9193
--max-instances 8 \
9294
--allow-unauthenticated

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ jobs:
112112
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
113113
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
114114
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
115+
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
116+
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
115117
--platform "managed" \
116118
--max-instances 2 \
117119
--allow-unauthenticated

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ jobs:
111111
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
112112
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
113113
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
114+
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
115+
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
114116
--platform "managed" \
115117
--max-instances 2 \
116118
--allow-unauthenticated

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public static class ApplicationConfig {
3434
@NotNull
3535
private NotificationsConfig notifications;
3636

37+
@NotNull
38+
private PulseResponseConfig pulseResponse;
39+
3740
@Getter
3841
@Setter
3942
@ConfigurationProperties("feedback")
@@ -89,5 +92,25 @@ public static class SlackConfig {
8992
private String botToken;
9093
}
9194
}
95+
96+
@Getter
97+
@Setter
98+
@ConfigurationProperties("pulse-response")
99+
public static class PulseResponseConfig {
100+
101+
@NotNull
102+
private SlackConfig slack;
103+
104+
@Getter
105+
@Setter
106+
@ConfigurationProperties("slack")
107+
public static class SlackConfig {
108+
@NotBlank
109+
private String signingSecret;
110+
111+
@NotBlank
112+
private String botToken;
113+
}
114+
}
92115
}
93116
}

server/src/main/java/com/objectcomputing/checkins/services/pulse/PulseServices.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
import java.time.LocalDate;
44

55
public interface PulseServices {
6-
public void sendPendingEmail(LocalDate now);
6+
public void notifyUsers(LocalDate now);
77
}

server/src/main/java/com/objectcomputing/checkins/services/pulse/PulseServicesImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public PulseServicesImpl(
6969
this.automatedEmailRepository = automatedEmailRepository;
7070
}
7171

72-
public void sendPendingEmail(LocalDate check) {
72+
public void notifyUsers(LocalDate check) {
7373
if (check.getDayOfWeek() == emailDay) {
7474
LOG.info("Checking for pending Pulse email");
7575
// Start from the first of the year and move forward to ensure that we

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package com.objectcomputing.checkins.services.pulseresponse;
22

33
import com.objectcomputing.checkins.exceptions.NotFoundException;
4+
import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder;
5+
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
6+
7+
import io.micronaut.http.MediaType;
48
import io.micronaut.core.annotation.Nullable;
59
import io.micronaut.core.convert.format.Format;
10+
import io.micronaut.http.HttpStatus;
611
import io.micronaut.http.HttpRequest;
712
import io.micronaut.http.HttpResponse;
13+
import io.micronaut.http.annotation.Header;
814
import io.micronaut.http.annotation.Body;
915
import io.micronaut.http.annotation.Controller;
1016
import io.micronaut.http.annotation.Get;
@@ -14,6 +20,7 @@
1420
import io.micronaut.scheduling.annotation.ExecuteOn;
1521
import io.micronaut.security.annotation.Secured;
1622
import io.micronaut.security.rules.SecurityRule;
23+
1724
import io.swagger.v3.oas.annotations.tags.Tag;
1825
import jakarta.validation.Valid;
1926
import jakarta.validation.constraints.NotNull;
@@ -22,17 +29,27 @@
2229
import java.time.LocalDate;
2330
import java.util.Set;
2431
import java.util.UUID;
32+
import java.util.Map;
33+
import java.nio.charset.StandardCharsets;
2534

2635
@Controller("/services/pulse-responses")
2736
@ExecuteOn(TaskExecutors.BLOCKING)
28-
@Secured(SecurityRule.IS_AUTHENTICATED)
2937
@Tag(name = "pulse-responses")
3038
public class PulseResponseController {
3139

3240
private final PulseResponseService pulseResponseServices;
41+
private final MemberProfileServices memberProfileServices;
42+
private final SlackSignatureVerifier slackSignatureVerifier;
43+
private final PulseSlackCommand pulseSlackCommand;
3344

34-
public PulseResponseController(PulseResponseService pulseResponseServices) {
45+
public PulseResponseController(PulseResponseService pulseResponseServices,
46+
MemberProfileServices memberProfileServices,
47+
SlackSignatureVerifier slackSignatureVerifier,
48+
PulseSlackCommand pulseSlackCommand) {
3549
this.pulseResponseServices = pulseResponseServices;
50+
this.memberProfileServices = memberProfileServices;
51+
this.slackSignatureVerifier = slackSignatureVerifier;
52+
this.pulseSlackCommand = pulseSlackCommand;
3653
}
3754

3855
/**
@@ -43,6 +60,7 @@ public PulseResponseController(PulseResponseService pulseResponseServices) {
4360
* @param dateTo
4461
* @return
4562
*/
63+
@Secured(SecurityRule.IS_AUTHENTICATED)
4664
@Get("/{?teamMemberId,dateFrom,dateTo}")
4765
public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") LocalDate dateFrom,
4866
@Nullable @Format("yyyy-MM-dd") LocalDate dateTo,
@@ -56,6 +74,7 @@ public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") Loc
5674
* @param pulseResponse, {@link PulseResponseCreateDTO}
5775
* @return {@link HttpResponse<PulseResponse>}
5876
*/
77+
@Secured(SecurityRule.IS_AUTHENTICATED)
5978
@Post
6079
public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseResponseCreateDTO pulseResponse,
6180
HttpRequest<?> request) {
@@ -70,6 +89,7 @@ public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseRespons
7089
* @param pulseResponse, {@link PulseResponse}
7190
* @return {@link HttpResponse<PulseResponse>}
7291
*/
92+
@Secured(SecurityRule.IS_AUTHENTICATED)
7393
@Put
7494
public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pulseResponse,
7595
HttpRequest<?> request) {
@@ -82,6 +102,7 @@ public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pu
82102
* @param id
83103
* @return
84104
*/
105+
@Secured(SecurityRule.IS_AUTHENTICATED)
85106
@Get("/{id}")
86107
public PulseResponse readRole(@NotNull UUID id) {
87108
PulseResponse result = pulseResponseServices.read(id);
@@ -90,4 +111,72 @@ public PulseResponse readRole(@NotNull UUID id) {
90111
}
91112
return result;
92113
}
93-
}
114+
115+
@Secured(SecurityRule.IS_ANONYMOUS)
116+
@Post(uri = "/command", consumes = MediaType.APPLICATION_FORM_URLENCODED)
117+
public HttpResponse commandPulseResponse(
118+
@Header("X-Slack-Signature") String signature,
119+
@Header("X-Slack-Request-Timestamp") String timestamp,
120+
@Body String requestBody) {
121+
// Validate the request
122+
if (slackSignatureVerifier.verifyRequest(signature,
123+
timestamp, requestBody)) {
124+
// Convert the request body to a map of values.
125+
FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder();
126+
Map<String, Object> body =
127+
formUrlEncodedDecoder.decode(requestBody,
128+
StandardCharsets.UTF_8);
129+
130+
// Respond to the slack command.
131+
String triggerId = (String)body.get("trigger_id");
132+
if (pulseSlackCommand.send(triggerId)) {
133+
return HttpResponse.ok();
134+
} else {
135+
return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
136+
}
137+
} else {
138+
return HttpResponse.unauthorized();
139+
}
140+
}
141+
142+
@Secured(SecurityRule.IS_ANONYMOUS)
143+
@Post("/external")
144+
public HttpResponse<PulseResponse> externalPulseResponse(
145+
@Header("X-Slack-Signature") String signature,
146+
@Header("X-Slack-Request-Timestamp") String timestamp,
147+
@Body String requestBody,
148+
HttpRequest<?> request) {
149+
// Validate the request
150+
if (slackSignatureVerifier.verifyRequest(signature,
151+
timestamp, requestBody)) {
152+
PulseResponseCreateDTO pulseResponseDTO =
153+
SlackPulseResponseConverter.get(memberProfileServices,
154+
requestBody);
155+
156+
// Create the pulse response
157+
PulseResponse pulseResponse = pulseResponseServices.unsecureSave(
158+
new PulseResponse(
159+
pulseResponseDTO.getInternalScore(),
160+
pulseResponseDTO.getExternalScore(),
161+
pulseResponseDTO.getSubmissionDate(),
162+
pulseResponseDTO.getTeamMemberId(),
163+
pulseResponseDTO.getInternalFeelings(),
164+
pulseResponseDTO.getExternalFeelings()
165+
)
166+
);
167+
168+
if (pulseResponse == null) {
169+
return HttpResponse.status(HttpStatus.CONFLICT,
170+
"Already submitted today");
171+
} else {
172+
return HttpResponse.created(pulseResponse)
173+
.headers(headers -> headers.location(
174+
URI.create(String.format("%s/%s",
175+
request.getPath(),
176+
pulseResponse.getId()))));
177+
}
178+
} else {
179+
return HttpResponse.unauthorized();
180+
}
181+
}
182+
}

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseRepository.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import jakarta.validation.constraints.NotNull;
77

88
import java.time.LocalDate;
9+
import java.util.Optional;
910
import java.util.List;
1011
import java.util.UUID;
1112

@@ -14,4 +15,5 @@ public interface PulseResponseRepository extends CrudRepository<PulseResponse, U
1415

1516
List<PulseResponse> findByTeamMemberId(@NotNull UUID teamMemberId);
1617
List<PulseResponse> findBySubmissionDateBetween(@NotNull LocalDate dateFrom, @NotNull LocalDate dateTo);
17-
}
18+
Optional<PulseResponse> getByTeamMemberIdAndSubmissionDate(@NotNull UUID teamMemberId, @NotNull LocalDate submissionDate);
19+
}

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ public interface PulseResponseService {
99
PulseResponse read(UUID id);
1010

1111
PulseResponse save(PulseResponse pulseResponse);
12+
PulseResponse unsecureSave(PulseResponse pulseResponse);
1213

1314
PulseResponse update(PulseResponse pulseResponse);
1415

1516
Set<PulseResponse> findByFields(UUID teamMemberId, LocalDate dateFrom, LocalDate dateTo);
16-
}
17+
}

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,61 @@ public PulseResponseServicesImpl(
4848

4949
@Override
5050
public PulseResponse save(PulseResponse pulseResponse) {
51-
UUID currentUserId = currentUserServices.getCurrentUser().getId();
51+
if (pulseResponse != null) {
52+
verifyPulseData(pulseResponse);
53+
final UUID memberId = pulseResponse.getTeamMemberId();
54+
UUID currentUserId = currentUserServices.getCurrentUser().getId();
55+
if (memberId != null &&
56+
!currentUserId.equals(memberId) &&
57+
!isSubordinateTo(memberId, currentUserId)) {
58+
throw new BadArgException(
59+
String.format("User %s does not have permission to create pulse response for user %s",
60+
currentUserId, memberId));
61+
}
62+
return saveCommon(pulseResponse);
63+
} else {
64+
return null;
65+
}
66+
}
67+
68+
@Override
69+
public PulseResponse unsecureSave(PulseResponse pulseResponse) {
5270
PulseResponse pulseResponseRet = null;
5371
if (pulseResponse != null) {
72+
// External users could submit a pulse resonse multiple times. We
73+
// need to check to see if this user has already submitted one
74+
// today.
75+
boolean submitted = false;
5476
final UUID memberId = pulseResponse.getTeamMemberId();
55-
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
56-
if (pulseResponse.getId() != null) {
57-
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
58-
} else if (memberId != null &&
59-
memberRepo.findById(memberId).isEmpty()) {
60-
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
61-
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
62-
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
63-
} else if (memberId != null &&
64-
!currentUserId.equals(memberId) &&
65-
!isSubordinateTo(memberId, currentUserId)) {
66-
throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId));
77+
if (memberId != null) {
78+
Optional<PulseResponse> existing =
79+
pulseResponseRepo.getByTeamMemberIdAndSubmissionDate(
80+
memberId, pulseResponse.getSubmissionDate());
81+
submitted = existing.isPresent();
82+
}
83+
if (!submitted) {
84+
verifyPulseData(pulseResponse);
85+
return saveCommon(pulseResponse);
6786
}
68-
pulseResponseRet = pulseResponseRepo.save(pulseResponse);
6987
}
88+
return null;
89+
}
90+
91+
private void verifyPulseData(PulseResponse pulseResponse) {
92+
final UUID memberId = pulseResponse.getTeamMemberId();
93+
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
94+
if (pulseResponse.getId() != null) {
95+
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
96+
} else if (memberId != null &&
97+
memberRepo.findById(memberId).isEmpty()) {
98+
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
99+
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
100+
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
101+
}
102+
}
103+
104+
private PulseResponse saveCommon(PulseResponse pulseResponse) {
105+
PulseResponse pulseResponseRet = pulseResponseRepo.save(pulseResponse);
70106

71107
// Send low pulse survey score if appropriate
72108
sendPulseLowScoreEmail(pulseResponseRet);

0 commit comments

Comments
 (0)