Skip to content

Commit

Permalink
Initial version of Pulse Response submission through Slack.
Browse files Browse the repository at this point in the history
  • Loading branch information
ocielliottc committed Jan 10, 2025
1 parent 7fe227e commit cf2a90b
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public static class ApplicationConfig {
@NotNull
private NotificationsConfig notifications;

@NotNull
private PulseResponseConfig pulseResponse;

@Getter
@Setter
@ConfigurationProperties("feedback")
Expand Down Expand Up @@ -89,5 +92,25 @@ public static class SlackConfig {
private String botToken;
}
}

@Getter
@Setter
@ConfigurationProperties("pulse-response")
public static class PulseResponseConfig {

@NotNull
private SlackConfig slack;

@Getter
@Setter
@ConfigurationProperties("slack")
public static class SlackConfig {
@NotBlank
private String signingSecret;

@NotBlank
private String webhookUrl;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.objectcomputing.checkins.services.pulseresponse;

import com.objectcomputing.checkins.exceptions.NotFoundException;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.format.Format;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
Expand All @@ -14,6 +18,7 @@
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
Expand All @@ -25,14 +30,19 @@

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

private final PulseResponseService pulseResponseServices;
private final MemberProfileServices memberProfileServices;
private final SlackSignatureVerifier slackSignatureVerifier;

public PulseResponseController(PulseResponseService pulseResponseServices) {
public PulseResponseController(PulseResponseService pulseResponseServices,
MemberProfileServices memberProfileServices,
SlackSignatureVerifier slackSignatureVerifier) {
this.pulseResponseServices = pulseResponseServices;
this.memberProfileServices = memberProfileServices;
this.slackSignatureVerifier = slackSignatureVerifier;
}

/**
Expand All @@ -43,6 +53,7 @@ public PulseResponseController(PulseResponseService pulseResponseServices) {
* @param dateTo
* @return
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Get("/{?teamMemberId,dateFrom,dateTo}")
public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") LocalDate dateFrom,
@Nullable @Format("yyyy-MM-dd") LocalDate dateTo,
Expand All @@ -56,6 +67,7 @@ public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") Loc
* @param pulseResponse, {@link PulseResponseCreateDTO}
* @return {@link HttpResponse<PulseResponse>}
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Post
public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseResponseCreateDTO pulseResponse,
HttpRequest<?> request) {
Expand All @@ -70,6 +82,7 @@ public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseRespons
* @param pulseResponse, {@link PulseResponse}
* @return {@link HttpResponse<PulseResponse>}
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Put
public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pulseResponse,
HttpRequest<?> request) {
Expand All @@ -82,6 +95,7 @@ public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pu
* @param id
* @return
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Get("/{id}")
public PulseResponse readRole(@NotNull UUID id) {
PulseResponse result = pulseResponseServices.read(id);
Expand All @@ -90,4 +104,45 @@ public PulseResponse readRole(@NotNull UUID id) {
}
return result;
}
}

@Secured(SecurityRule.IS_ANONYMOUS)
@Post("/external")
public HttpResponse<PulseResponse> externalPulseResponse(
@Header("X-Slack-Signature") String signature,
@Header("X-Slack-Request-Timestamp") String timestamp,
@Body String requestBody,
HttpRequest<?> request) {
// Validate the request
if (slackSignatureVerifier.verifyRequest(signature,
timestamp, requestBody)) {
PulseResponseCreateDTO pulseResponseDTO =
SlackPulseResponseConverter.get(memberProfileServices,
requestBody);

// Create the pulse response
PulseResponse pulseResponse = pulseResponseServices.unsecureSave(
new PulseResponse(
pulseResponseDTO.getInternalScore(),
pulseResponseDTO.getExternalScore(),
pulseResponseDTO.getSubmissionDate(),
pulseResponseDTO.getTeamMemberId(),
pulseResponseDTO.getInternalFeelings(),
pulseResponseDTO.getExternalFeelings()
)
);

if (pulseResponse == null) {
return HttpResponse.status(HttpStatus.CONFLICT,
"Already submitted today");
} else {
return HttpResponse.created(pulseResponse)
.headers(headers -> headers.location(
URI.create(String.format("%s/%s",
request.getPath(),
pulseResponse.getId()))));
}
} else {
return HttpResponse.unauthorized();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.constraints.NotNull;

import java.time.LocalDate;
import java.util.Optional;
import java.util.List;
import java.util.UUID;

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

List<PulseResponse> findByTeamMemberId(@NotNull UUID teamMemberId);
List<PulseResponse> findBySubmissionDateBetween(@NotNull LocalDate dateFrom, @NotNull LocalDate dateTo);
}
Optional<PulseResponse> getByTeamMemberIdAndSubmissionDate(@NotNull UUID teamMemberId, @NotNull LocalDate submissionDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ public interface PulseResponseService {
PulseResponse read(UUID id);

PulseResponse save(PulseResponse pulseResponse);
PulseResponse unsecureSave(PulseResponse pulseResponse);

PulseResponse update(PulseResponse pulseResponse);

Set<PulseResponse> findByFields(UUID teamMemberId, LocalDate dateFrom, LocalDate dateTo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,57 @@ public PulseResponseServicesImpl(

@Override
public PulseResponse save(PulseResponse pulseResponse) {
UUID currentUserId = currentUserServices.getCurrentUser().getId();
if (pulseResponse != null) {
final UUID memberId = pulseResponse.getTeamMemberId();
UUID currentUserId = currentUserServices.getCurrentUser().getId();
if (memberId != null &&
!currentUserId.equals(memberId) &&
!isSubordinateTo(memberId, currentUserId)) {
throw new BadArgException(
String.format("User %s does not have permission to create pulse response for user %s",
currentUserId, memberId));
}
return saveCommon(pulseResponse);
} else {
return null;
}
}

@Override
public PulseResponse unsecureSave(PulseResponse pulseResponse) {
PulseResponse pulseResponseRet = null;
if (pulseResponse != null) {
// External users could submit a pulse resonse multiple times. We
// need to check to see if this user has already submitted one
// today.
boolean submitted = false;
final UUID memberId = pulseResponse.getTeamMemberId();
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
if (pulseResponse.getId() != null) {
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
} else if (memberId != null &&
memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
} else if (memberId != null &&
!currentUserId.equals(memberId) &&
!isSubordinateTo(memberId, currentUserId)) {
throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId));
if (memberId != null) {
Optional<PulseResponse> existing =
pulseResponseRepo.getByTeamMemberIdAndSubmissionDate(
memberId, pulseResponse.getSubmissionDate());
submitted = existing.isPresent();
}
if (!submitted) {
return saveCommon(pulseResponse);
}
pulseResponseRet = pulseResponseRepo.save(pulseResponse);
}
return null;
}

private PulseResponse saveCommon(PulseResponse pulseResponse) {
final UUID memberId = pulseResponse.getTeamMemberId();
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
if (pulseResponse.getId() != null) {
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
} else if (memberId != null &&
memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
}

PulseResponse pulseResponseRet = pulseResponseRepo.save(pulseResponse);

// Send low pulse survey score if appropriate
sendPulseLowScoreEmail(pulseResponseRet);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.objectcomputing.checkins.services.pulseresponse;

import com.objectcomputing.checkins.exceptions.BadArgException;
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.core.JsonProcessingException;

import java.util.Map;
import java.util.UUID;
import java.time.LocalDate;

public class SlackPulseResponseConverter {
public static PulseResponseCreateDTO get(
MemberProfileServices memberProfileServices, String body) {
final String key = "payload=";
final int start = body.indexOf(key);
if (start >= 0) {
try {
// Get the map of values from the string body
final ObjectMapper mapper = new ObjectMapper();
final Map<String, Object> map =
mapper.readValue(body.substring(start + key.length()),
new TypeReference<>() {});
final Map<String, Object> view =
(Map<String, Object>)map.get("view");
final Map<String, Object> state =
(Map<String, Object>)view.get("state");
final Map<String, Object> values =
(Map<String, Object>)state.get("values");

// Create the pulse DTO and fill in the values.
PulseResponseCreateDTO response = new PulseResponseCreateDTO();
response.setTeamMemberId(lookupUser(memberProfileServices, map));
response.setSubmissionDate(LocalDate.now());
response.setInternalScore(Integer.parseInt(
getMappedValue(values, "internalScore")));
response.setInternalFeelings(
getMappedValue(values, "internalFeelings"));
response.setExternalScore(Integer.parseInt(
getMappedValue(values, "externalScore")));
response.setExternalFeelings(
getMappedValue(values, "externalFeelings"));

return response;
} catch(JsonProcessingException ex) {
throw new BadArgException(ex.getMessage());
}
} else {
throw new BadArgException("Invalid pulse response body");
}
}

private static String getMappedValue(Map<String, Object> map, String key) {
return (String)((Map<String, Object>)map.get(key)).get("value");
}

private static UUID lookupUser(MemberProfileServices memberProfileServices,
Map<String, Object> map) {
// Get the user's profile map.
Map<String, Object> user = (Map<String, Object>)map.get("user");
Map<String, Object> profile = (Map<String, Object>)user.get("profile");

// Lookup the user based on the email address.
String email = (String)profile.get("email");
MemberProfile member = memberProfileServices.findByWorkEmail(email);
return member.getId();
}
}
Loading

0 comments on commit cf2a90b

Please sign in to comment.