Skip to content

Commit 4602de6

Browse files
authored
Merge branch 'develop' into feature-2645/feedback-request-add-ability-to-request-feedback-from-external-source
2 parents 72ef550 + 8b3855d commit 4602de6

File tree

9 files changed

+259
-122
lines changed

9 files changed

+259
-122
lines changed

server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import com.slack.api.methods.response.conversations.ConversationsListResponse;
1111
import com.slack.api.methods.request.users.UsersLookupByEmailRequest;
1212
import com.slack.api.methods.response.users.UsersLookupByEmailResponse;
13+
import com.slack.api.methods.request.users.UsersInfoRequest;
14+
import com.slack.api.methods.response.users.UsersInfoResponse;
1315

1416
import jakarta.inject.Singleton;
1517
import jakarta.inject.Inject;
@@ -76,5 +78,26 @@ public String findUserId(String userEmail) {
7678
}
7779
return null;
7880
}
81+
82+
public String findUserEmail(String userId) {
83+
String token = configuration.getApplication().getNotifications().getSlack().getBotToken();
84+
if (token != null) {
85+
try {
86+
MethodsClient client = Slack.getInstance().methods(token);
87+
UsersInfoResponse response = client.usersInfo(
88+
UsersInfoRequest.builder().user(userId).build()
89+
);
90+
91+
if (response.isOk()) {
92+
return response.getUser().getProfile().getEmail();
93+
}
94+
} catch(IOException e) {
95+
LOG.error("SlackSearch.findUserEmail: " + e.toString());
96+
} catch(SlackApiException e) {
97+
LOG.error("SlackSearch.findUserEmail: " + e.toString());
98+
}
99+
}
100+
return null;
101+
}
79102
}
80103

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

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public class PulseResponse {
7777
protected PulseResponse() {
7878
}
7979

80-
public PulseResponse(UUID id, Integer internalScore, @Nullable Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, String internalFeelings, String externalFeelings) {
80+
public PulseResponse(UUID id, Integer internalScore, @Nullable Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, @Nullable String internalFeelings, @Nullable String externalFeelings) {
8181
this.id = id;
8282
this.internalScore = internalScore;
8383
this.externalScore = externalScore;
@@ -91,62 +91,6 @@ public PulseResponse(Integer internalScore, Integer externalScore, LocalDate sub
9191
this(null, internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings);
9292
}
9393

94-
public UUID getId() {
95-
return this.id;
96-
}
97-
98-
public void setId(UUID id) {
99-
this.id = id;
100-
}
101-
102-
public Integer getInternalScore() {
103-
return internalScore;
104-
}
105-
106-
public void setInternalScore(Integer internalScore) {
107-
this.internalScore = internalScore;
108-
}
109-
110-
public Integer getExternalScore() {
111-
return externalScore;
112-
}
113-
114-
public void setExternalScore(Integer externalScore) {
115-
this.externalScore = externalScore;
116-
}
117-
118-
public LocalDate getSubmissionDate() {
119-
return submissionDate;
120-
}
121-
122-
public void setSubmissionDate(LocalDate submissionDate) {
123-
this.submissionDate = submissionDate;
124-
}
125-
126-
public UUID getTeamMemberId() {
127-
return this.teamMemberId;
128-
}
129-
130-
public void setTeamMemberId(UUID teamMemberId) {
131-
this.teamMemberId = teamMemberId;
132-
}
133-
134-
public String getInternalFeelings() {
135-
return internalFeelings;
136-
}
137-
138-
public void setInternalFeelings(String internalFeelings) {
139-
this.internalFeelings = internalFeelings;
140-
}
141-
142-
public String getExternalFeelings() {
143-
return externalFeelings;
144-
}
145-
146-
public void setExternalFeelings(String externalFeelings) {
147-
this.externalFeelings = externalFeelings;
148-
}
149-
15094
@Override
15195
public boolean equals(Object o) {
15296
if (this == o) return true;

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

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,22 @@
3636
@ExecuteOn(TaskExecutors.BLOCKING)
3737
@Tag(name = "pulse-responses")
3838
public class PulseResponseController {
39-
4039
private final PulseResponseService pulseResponseServices;
4140
private final MemberProfileServices memberProfileServices;
4241
private final SlackSignatureVerifier slackSignatureVerifier;
4342
private final PulseSlackCommand pulseSlackCommand;
43+
private final SlackPulseResponseConverter slackPulseResponseConverter;
4444

4545
public PulseResponseController(PulseResponseService pulseResponseServices,
4646
MemberProfileServices memberProfileServices,
4747
SlackSignatureVerifier slackSignatureVerifier,
48-
PulseSlackCommand pulseSlackCommand) {
48+
PulseSlackCommand pulseSlackCommand,
49+
SlackPulseResponseConverter slackPulseResponseConverter) {
4950
this.pulseResponseServices = pulseResponseServices;
5051
this.memberProfileServices = memberProfileServices;
5152
this.slackSignatureVerifier = slackSignatureVerifier;
5253
this.pulseSlackCommand = pulseSlackCommand;
54+
this.slackPulseResponseConverter = slackPulseResponseConverter;
5355
}
5456

5557
/**
@@ -141,39 +143,55 @@ public HttpResponse commandPulseResponse(
141143

142144
@Secured(SecurityRule.IS_ANONYMOUS)
143145
@Post(uri = "/external", consumes = MediaType.APPLICATION_FORM_URLENCODED)
144-
public HttpResponse<PulseResponse> externalPulseResponse(
146+
public HttpResponse externalPulseResponse(
145147
@Header("X-Slack-Signature") String signature,
146148
@Header("X-Slack-Request-Timestamp") String timestamp,
147149
@Body String requestBody,
148150
HttpRequest<?> request) {
149151
// Validate the request
150152
if (slackSignatureVerifier.verifyRequest(signature,
151153
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");
154+
// Convert the request body to a map of values.
155+
FormUrlEncodedDecoder formUrlEncodedDecoder =
156+
new FormUrlEncodedDecoder();
157+
Map<String, Object> body =
158+
formUrlEncodedDecoder.decode(requestBody,
159+
StandardCharsets.UTF_8);
160+
161+
final String key = "payload";
162+
if (body.containsKey(key)) {
163+
PulseResponseCreateDTO pulseResponseDTO =
164+
slackPulseResponseConverter.get(memberProfileServices,
165+
(String)body.get(key));
166+
167+
// If we receive a null DTO, that means that this is not the
168+
// actual submission of the form. We can just return 200 so
169+
// that Slack knows to continue without error.
170+
if (pulseResponseDTO == null) {
171+
return HttpResponse.ok();
172+
}
173+
174+
// Create the pulse response
175+
PulseResponse pulseResponse =
176+
pulseResponseServices.unsecureSave(
177+
new PulseResponse(
178+
pulseResponseDTO.getInternalScore(),
179+
pulseResponseDTO.getExternalScore(),
180+
pulseResponseDTO.getSubmissionDate(),
181+
pulseResponseDTO.getTeamMemberId(),
182+
pulseResponseDTO.getInternalFeelings(),
183+
pulseResponseDTO.getExternalFeelings()
184+
)
185+
);
186+
187+
if (pulseResponse == null) {
188+
return HttpResponse.status(HttpStatus.CONFLICT,
189+
"Already submitted today");
190+
} else {
191+
return HttpResponse.ok();
192+
}
171193
} else {
172-
return HttpResponse.created(pulseResponse)
173-
.headers(headers -> headers.location(
174-
URI.create(String.format("%s/%s",
175-
request.getPath(),
176-
pulseResponse.getId()))));
194+
return HttpResponse.unprocessableEntity();
177195
}
178196
} else {
179197
return HttpResponse.unauthorized();

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

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import com.objectcomputing.checkins.exceptions.BadArgException;
44
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
55
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
6+
import com.objectcomputing.checkins.notifications.social_media.SlackSearch;
7+
8+
import jakarta.inject.Singleton;
9+
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
612

713
import com.fasterxml.jackson.databind.ObjectMapper;
814
import com.fasterxml.jackson.core.type.TypeReference;
@@ -12,18 +18,26 @@
1218
import java.util.UUID;
1319
import java.time.LocalDate;
1420

21+
@Singleton
1522
public class SlackPulseResponseConverter {
16-
public static PulseResponseCreateDTO get(
23+
private static final Logger LOG = LoggerFactory.getLogger(SlackPulseResponseConverter.class);
24+
25+
private final SlackSearch slackSearch;
26+
27+
public SlackPulseResponseConverter(SlackSearch slackSearch) {
28+
this.slackSearch = slackSearch;
29+
}
30+
31+
public PulseResponseCreateDTO get(
1732
MemberProfileServices memberProfileServices, String body) {
18-
final String key = "payload=";
19-
final int start = body.indexOf(key);
20-
if (start >= 0) {
21-
try {
22-
// Get the map of values from the string body
23-
final ObjectMapper mapper = new ObjectMapper();
24-
final Map<String, Object> map =
25-
mapper.readValue(body.substring(start + key.length()),
26-
new TypeReference<>() {});
33+
try {
34+
// Get the map of values from the string body
35+
final ObjectMapper mapper = new ObjectMapper();
36+
final Map<String, Object> map =
37+
mapper.readValue(body, new TypeReference<>() {});
38+
final String type = (String)map.get("type");
39+
40+
if (type.equals("view_submission")) {
2741
final Map<String, Object> view =
2842
(Map<String, Object>)map.get("view");
2943
final Map<String, Object> state =
@@ -36,55 +50,77 @@ public static PulseResponseCreateDTO get(
3650
response.setTeamMemberId(lookupUser(memberProfileServices, map));
3751
response.setSubmissionDate(LocalDate.now());
3852

39-
response.setInternalScore(Integer.parseInt(
40-
getMappedValue(values, "internalScore", true)));
41-
response.setInternalFeelings(
42-
getMappedValue(values, "internalFeelings", false));
53+
// Internal Score
54+
Map<String, Object> internalBlock =
55+
(Map<String, Object>)values.get("internalNumber");
56+
response.setInternalScore(Integer.parseInt(getMappedValue(
57+
internalBlock, "internalScore", "selected_option", true)));
58+
// Internal Feelings
59+
response.setInternalFeelings(getMappedValue(
60+
values, "internalText", "internalFeelings", false));
4361

44-
String score = getMappedValue(values, "externalScore", false);
45-
if (!score.isEmpty()) {
62+
// External Score
63+
Map<String, Object> externalBlock =
64+
(Map<String, Object>)values.get("externalNumber");
65+
String score = getMappedValue(externalBlock, "externalScore",
66+
"selected_option", false);
67+
if (score != null && !score.isEmpty()) {
4668
response.setExternalScore(Integer.parseInt(score));
4769
}
48-
response.setExternalFeelings(
49-
getMappedValue(values, "externalFeelings", false));
70+
// External Feelings
71+
response.setExternalFeelings(getMappedValue(
72+
values, "externalText", "externalFeelings", false));
5073

5174
return response;
52-
} catch(JsonProcessingException ex) {
53-
throw new BadArgException(ex.getMessage());
54-
} catch(NumberFormatException ex) {
55-
throw new BadArgException("Pulse scores must be integers");
75+
} else {
76+
// If it's not a view submission, we need to return null so
77+
// the the caller knows that this is not the full pulse
78+
// response.
79+
return null;
5680
}
57-
} else {
58-
throw new BadArgException("Invalid pulse response body");
81+
} catch(JsonProcessingException ex) {
82+
LOG.error(ex.getMessage());
83+
throw new BadArgException(ex.getMessage());
84+
} catch(NumberFormatException ex) {
85+
LOG.error(ex.getMessage());
86+
throw new BadArgException("Pulse scores must be integers");
5987
}
6088
}
6189

62-
private static String getMappedValue(Map<String, Object> map,
63-
String key, boolean required) {
90+
private String getMappedValue(Map<String, Object> map, String key1,
91+
String key2, boolean required) {
6492
final String valueKey = "value";
65-
if (map.containsKey(key)) {
66-
final Map<String, Object> other = (Map<String, Object>)map.get(key);
67-
if (other.containsKey(valueKey)) {
68-
return (String)other.get(valueKey);
93+
if (map != null && map.containsKey(key1)) {
94+
Map<String, Object> firstMap = (Map<String, Object>)map.get(key1);
95+
if (firstMap != null && firstMap.containsKey(key2)) {
96+
final Map<String, Object> secondMap =
97+
(Map<String, Object>)firstMap.get(key2);
98+
if (secondMap != null && secondMap.containsKey(valueKey)) {
99+
return (String)secondMap.get(valueKey);
100+
}
69101
}
70102
}
71103

72104
if (required) {
105+
LOG.error("Expected {}.{}.{} was not found", key1, key2, valueKey);
73106
throw new BadArgException(
74-
String.format("Expected %s.%s was not found", key, valueKey));
107+
String.format("Expected %s.%s.%s was not found",
108+
key1, key2, valueKey));
75109
} else {
76-
return "";
110+
return null;
77111
}
78112
}
79113

80-
private static UUID lookupUser(MemberProfileServices memberProfileServices,
81-
Map<String, Object> map) {
114+
private UUID lookupUser(MemberProfileServices memberProfileServices,
115+
Map<String, Object> map) {
82116
// Get the user's profile map.
83117
Map<String, Object> user = (Map<String, Object>)map.get("user");
84-
Map<String, Object> profile = (Map<String, Object>)user.get("profile");
85118

86119
// Lookup the user based on the email address.
87-
String email = (String)profile.get("email");
120+
String email = slackSearch.findUserEmail((String)user.get("id"));
121+
if (email == null) {
122+
throw new BadArgException("Unable to find the user email address");
123+
}
88124
MemberProfile member = memberProfileServices.findByWorkEmail(email);
89125
return member.getId();
90126
}

server/src/main/resources/slack/pulse_slack_blocks.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"blocks": [
1919
{
2020
"type": "section",
21+
"block_id": "internalNumber",
2122
"text": {
2223
"type": "plain_text",
2324
"text": "How are you feeling about work today?"
@@ -71,6 +72,7 @@
7172
},
7273
{
7374
"type": "input",
75+
"block_id": "internalText",
7476
"optional": true,
7577
"element": {
7678
"type": "plain_text_input",
@@ -88,6 +90,7 @@
8890
},
8991
{
9092
"type": "section",
93+
"block_id": "externalNumber",
9194
"text": {
9295
"type": "plain_text",
9396
"text": "How are you feeling about life outside of work?"
@@ -141,6 +144,7 @@
141144
},
142145
{
143146
"type": "input",
147+
"block_id": "externalText",
144148
"optional": true,
145149
"element": {
146150
"type": "plain_text_input",

0 commit comments

Comments
 (0)