Skip to content

Commit e9b030d

Browse files
authored
Merge pull request #2818 from objectcomputing/develop
Update 0.8 from develop
2 parents d7017bf + 1ddde8d commit e9b030d

File tree

68 files changed

+2992
-3213
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2992
-3213
lines changed

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

+2
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

+2
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

+2
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

.github/workflows/jekyll.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Checkout
3535
uses: actions/checkout@v4
3636
- name: Setup Ruby
37-
uses: ruby/setup-ruby@8575951200e472d5f2d95c625da0c7bec8217c42 # v1.161.0
37+
uses: ruby/setup-ruby@v1
3838
with:
3939
working-directory: docs
4040
ruby-version: '3.3' # Not needed with a .ruby-version file

server/build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
id "jacoco"
88
}
99

10-
version "0.8.11"
10+
version "0.8.12"
1111
group "com.objectcomputing.checkins"
1212

1313
repositories {
@@ -79,6 +79,7 @@ dependencies {
7979

8080
yarnBuildElements(project(":web-ui"))
8181

82+
implementation("net.steppschuh.markdowngenerator:markdowngenerator:1.3.1.1")
8283
implementation("io.micronaut:micronaut-jackson-databind")
8384
implementation("io.micronaut:micronaut-http-client")
8485
implementation("io.micronaut:micronaut-management")
@@ -116,6 +117,7 @@ dependencies {
116117
implementation("io.micrometer:context-propagation")
117118

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

120122
testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
121123
testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion"

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

+23
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
}
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+
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+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.objectcomputing.checkins.services.file;
2+
3+
import com.objectcomputing.checkins.services.checkindocument.CheckinDocument;
4+
import com.objectcomputing.checkins.services.checkindocument.CheckinDocumentServices;
5+
import com.objectcomputing.checkins.services.checkins.CheckIn;
6+
import com.objectcomputing.checkins.services.checkins.CheckInServices;
7+
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
8+
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
9+
import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils;
10+
import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices;
11+
12+
import io.micronaut.core.annotation.Nullable;
13+
import io.micronaut.http.multipart.CompletedFileUpload;
14+
15+
import jakarta.inject.Singleton;
16+
import jakarta.validation.constraints.NotNull;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import java.io.FileOutputStream;
21+
import java.io.IOException;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.Set;
25+
import java.util.UUID;
26+
import java.util.function.Function;
27+
28+
import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG;
29+
30+
@Singleton
31+
abstract public class FileServicesBaseImpl implements FileServices {
32+
private static final Logger LOG = LoggerFactory.getLogger(FileServicesBaseImpl.class);
33+
34+
protected final CheckInServices checkInServices;
35+
protected final CheckinDocumentServices checkinDocumentServices;
36+
protected final MemberProfileServices memberProfileServices;
37+
protected final CurrentUserServices currentUserServices;
38+
39+
public FileServicesBaseImpl(CheckInServices checkInServices,
40+
CheckinDocumentServices checkinDocumentServices,
41+
MemberProfileServices memberProfileServices,
42+
CurrentUserServices currentUserServices) {
43+
this.checkInServices = checkInServices;
44+
this.checkinDocumentServices = checkinDocumentServices;
45+
this.memberProfileServices = memberProfileServices;
46+
this.currentUserServices = currentUserServices;
47+
}
48+
49+
abstract protected void getCheckinDocuments(
50+
Set<FileInfoDTO> result, Set<CheckinDocument> checkinDocuments) throws IOException;
51+
abstract protected void downloadSingleFile(
52+
String docId, FileOutputStream myWriter) throws IOException;
53+
abstract protected FileInfoDTO uploadSingleFile(
54+
CompletedFileUpload file, String directoryName,
55+
Function<String, CheckinDocument> consumer) throws IOException;
56+
abstract protected void deleteSingleFile(String docId) throws IOException;
57+
58+
@Override
59+
public Set<FileInfoDTO> findFiles(@Nullable UUID checkInID) {
60+
boolean isAdmin = currentUserServices.isAdmin();
61+
validate(checkInID == null && !isAdmin, NOT_AUTHORIZED_MSG);
62+
63+
try {
64+
Set<FileInfoDTO> result = new HashSet<>();
65+
if (checkInID == null && isAdmin) {
66+
getCheckinDocuments(result, Collections.emptySet());
67+
} else if (checkInID != null) {
68+
validate(!checkInServices.accessGranted(checkInID, currentUserServices.getCurrentUser().getId()),
69+
"You are not authorized to perform this operation");
70+
71+
// If there aren't any documents, do not call
72+
// getCheckinDocument. It assumes that an empty set means
73+
// that it should attempt to get all documents. And, in this
74+
// case, we just want an empty result set.
75+
Set<CheckinDocument> checkinDocuments = checkinDocumentServices.read(checkInID);
76+
if (!checkinDocuments.isEmpty()) {
77+
getCheckinDocuments(result, checkinDocuments);
78+
}
79+
}
80+
81+
return result;
82+
} catch (IOException e) {
83+
LOG.error("Error occurred while retrieving files.", e);
84+
throw new FileRetrievalException(e.getMessage());
85+
}
86+
}
87+
88+
@Override
89+
public java.io.File downloadFiles(@NotNull String uploadDocId) {
90+
MemberProfile currentUser = currentUserServices.getCurrentUser();
91+
boolean isAdmin = currentUserServices.isAdmin();
92+
93+
CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId);
94+
validate(cd == null, String.format("Unable to find record with id %s", uploadDocId));
95+
96+
CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId());
97+
98+
if(!isAdmin) {
99+
validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG);
100+
}
101+
try {
102+
java.io.File file = java.io.File.createTempFile("tmp", ".txt");
103+
file.deleteOnExit();
104+
try(
105+
FileOutputStream myWriter = new FileOutputStream(file)
106+
) {
107+
downloadSingleFile(uploadDocId, myWriter);
108+
return file;
109+
} catch (IOException e) {
110+
LOG.error("Error occurred while retrieving files.", e);
111+
throw new FileRetrievalException(e.getMessage());
112+
}
113+
} catch(IOException e) {
114+
LOG.error("Error occurred while attempting to create a temporary file.", e);
115+
throw new FileRetrievalException(e.getMessage());
116+
}
117+
}
118+
119+
@Override
120+
public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpload file) {
121+
MemberProfile currentUser = currentUserServices.getCurrentUser();
122+
boolean isAdmin = currentUserServices.isAdmin();
123+
validate((file.getFilename() == null || file.getFilename().equals("")), "Please select a valid file before uploading.");
124+
125+
CheckIn checkIn = checkInServices.read(checkInID);
126+
validate(checkIn == null, "Unable to find checkin record with id %s", checkInID);
127+
if(!isAdmin) {
128+
validate((!currentUser.getId().equals(checkIn.getTeamMemberId()) && !currentUser.getId().equals(checkIn.getPdlId())), "You are not authorized to perform this operation");
129+
validate(checkIn.isCompleted(), NOT_AUTHORIZED_MSG);
130+
}
131+
132+
// create folder for each team member
133+
final String directoryName = MemberProfileUtils.getFullName(memberProfileServices.getById(checkIn.getTeamMemberId()));
134+
135+
try {
136+
return uploadSingleFile(file, directoryName,
137+
(fileId) -> {
138+
//create record in checkin-document service
139+
CheckinDocument cd = new CheckinDocument(checkInID, fileId);
140+
checkinDocumentServices.save(cd);
141+
return cd;
142+
});
143+
} catch (IOException e) {
144+
LOG.error("Unexpected error processing file upload.", e);
145+
throw new FileRetrievalException(e.getMessage());
146+
}
147+
}
148+
149+
@Override
150+
public boolean deleteFile(@NotNull String uploadDocId) {
151+
MemberProfile currentUser = currentUserServices.getCurrentUser();
152+
boolean isAdmin = currentUserServices.isAdmin();
153+
154+
CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId);
155+
validate(cd == null, String.format("Unable to find record with id %s", uploadDocId));
156+
157+
CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId());
158+
if(!isAdmin) {
159+
validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG);
160+
}
161+
162+
try {
163+
deleteSingleFile(uploadDocId);
164+
checkinDocumentServices.deleteByUploadDocId(uploadDocId);
165+
return true;
166+
} catch (IOException e) {
167+
LOG.error("Error occurred while deleting files.", e);
168+
throw new FileRetrievalException(e.getMessage());
169+
}
170+
}
171+
172+
protected void validate(boolean isError, String message, Object... args) {
173+
if(isError) {
174+
throw new FileRetrievalException(String.format(message, args));
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)