Skip to content

Commit

Permalink
Merge pull request #2818 from objectcomputing/develop
Browse files Browse the repository at this point in the history
Update 0.8 from develop
  • Loading branch information
mkimberlin authored Jan 10, 2025
2 parents d7017bf + 1ddde8d commit e9b030d
Show file tree
Hide file tree
Showing 68 changed files with 2,992 additions and 3,213 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/gradle-build-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ jobs:
--set-env-vars "FROM_ADDRESS=no-reply@objectcomputing.com" \
--set-env-vars "FROM_NAME=Check-Ins" \
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 8 \
--allow-unauthenticated
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ jobs:
--set-env-vars "FROM_ADDRESS=no-reply@objectcomputing.com" \
--set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 2 \
--allow-unauthenticated
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-native-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:
--set-env-vars "FROM_ADDRESS=kimberlinm@objectcomputing.com" \
--set-env-vars "FROM_NAME=Check-Ins - DEVELOP" \
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 2 \
--allow-unauthenticated
2 changes: 1 addition & 1 deletion .github/workflows/jekyll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@8575951200e472d5f2d95c625da0c7bec8217c42 # v1.161.0
uses: ruby/setup-ruby@v1
with:
working-directory: docs
ruby-version: '3.3' # Not needed with a .ruby-version file
Expand Down
4 changes: 3 additions & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
id "jacoco"
}

version "0.8.11"
version "0.8.12"
group "com.objectcomputing.checkins"

repositories {
Expand Down Expand Up @@ -79,6 +79,7 @@ dependencies {

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

implementation("net.steppschuh.markdowngenerator:markdowngenerator:1.3.1.1")
implementation("io.micronaut:micronaut-jackson-databind")
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut:micronaut-management")
Expand Down Expand Up @@ -116,6 +117,7 @@ dependencies {
implementation("io.micrometer:context-propagation")

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

testRuntimeOnly "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
testRuntimeOnly "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public static class ApplicationConfig {
@NotNull
private GoogleApiConfig googleApi;

@NotNull
private NotificationsConfig notifications;

@Getter
@Setter
@ConfigurationProperties("feedback")
Expand Down Expand Up @@ -66,5 +69,25 @@ public static class ScopeConfig {
private String scopeForDirectoryApi;
}
}

@Getter
@Setter
@ConfigurationProperties("notifications")
public static class NotificationsConfig {

@NotNull
private SlackConfig slack;

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

@NotBlank
private String botToken;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.objectcomputing.checkins.notifications.social_media;

import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;

import jakarta.inject.Singleton;
import jakarta.inject.Inject;

import java.util.List;

@Singleton
public class SlackPoster {
@Inject
private HttpClient slackClient;

@Inject
private CheckInsConfiguration configuration;

public HttpResponse post(String slackBlock) {
// See if we can have a webhook URL.
String slackWebHook = configuration.getApplication().getNotifications().getSlack().getWebhookUrl();
if (slackWebHook != null) {
// POST it to Slack.
BlockingHttpClient client = slackClient.toBlocking();
HttpRequest<String> request = HttpRequest.POST(slackWebHook,
slackBlock);
return client.exchange(request);
}
return HttpResponse.status(HttpStatus.GONE,
"Slack Webhook URL is not configured");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.objectcomputing.checkins.notifications.social_media;

import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.Slack;
import com.slack.api.methods.MethodsClient;
import com.slack.api.model.Conversation;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.conversations.ConversationsListRequest;
import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.request.users.UsersLookupByEmailRequest;
import com.slack.api.methods.response.users.UsersLookupByEmailResponse;

import jakarta.inject.Singleton;
import jakarta.inject.Inject;

import java.util.List;
import java.io.IOException;

import jnr.ffi.annotations.In;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class SlackSearch {
private static final Logger LOG = LoggerFactory.getLogger(SlackSearch.class);

private CheckInsConfiguration configuration;

public SlackSearch(CheckInsConfiguration checkInsConfiguration) {
this.configuration = checkInsConfiguration;
}

public String findChannelId(String channelName) {
String token = configuration.getApplication().getNotifications().getSlack().getBotToken();
if (token != null) {
try {
MethodsClient client = Slack.getInstance().methods(token);
ConversationsListResponse response = client.conversationsList(
ConversationsListRequest.builder().build()
);

if (response.isOk()) {
for (Conversation conversation: response.getChannels()) {
if (conversation.getName().equals(channelName)) {
return conversation.getId();
}
}
}
} catch(IOException e) {
LOG.error("SlackSearch.findChannelId: " + e.toString());
} catch(SlackApiException e) {
LOG.error("SlackSearch.findChannelId: " + e.toString());
}
}
return null;
}

public String findUserId(String userEmail) {
String token = configuration.getApplication().getNotifications().getSlack().getBotToken();
if (token != null) {
try {
MethodsClient client = Slack.getInstance().methods(token);
UsersLookupByEmailResponse response = client.usersLookupByEmail(
UsersLookupByEmailRequest.builder().email(userEmail).build()
);

if (response.isOk()) {
return response.getUser().getId();
}
} catch(IOException e) {
LOG.error("SlackSearch.findUserId: " + e.toString());
} catch(SlackApiException e) {
LOG.error("SlackSearch.findUserId: " + e.toString());
}
}
return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.objectcomputing.checkins.services.file;

import com.objectcomputing.checkins.services.checkindocument.CheckinDocument;
import com.objectcomputing.checkins.services.checkindocument.CheckinDocumentServices;
import com.objectcomputing.checkins.services.checkins.CheckIn;
import com.objectcomputing.checkins.services.checkins.CheckInServices;
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils;
import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.multipart.CompletedFileUpload;

import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;

import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG;

@Singleton
abstract public class FileServicesBaseImpl implements FileServices {
private static final Logger LOG = LoggerFactory.getLogger(FileServicesBaseImpl.class);

protected final CheckInServices checkInServices;
protected final CheckinDocumentServices checkinDocumentServices;
protected final MemberProfileServices memberProfileServices;
protected final CurrentUserServices currentUserServices;

public FileServicesBaseImpl(CheckInServices checkInServices,
CheckinDocumentServices checkinDocumentServices,
MemberProfileServices memberProfileServices,
CurrentUserServices currentUserServices) {
this.checkInServices = checkInServices;
this.checkinDocumentServices = checkinDocumentServices;
this.memberProfileServices = memberProfileServices;
this.currentUserServices = currentUserServices;
}

abstract protected void getCheckinDocuments(
Set<FileInfoDTO> result, Set<CheckinDocument> checkinDocuments) throws IOException;
abstract protected void downloadSingleFile(
String docId, FileOutputStream myWriter) throws IOException;
abstract protected FileInfoDTO uploadSingleFile(
CompletedFileUpload file, String directoryName,
Function<String, CheckinDocument> consumer) throws IOException;
abstract protected void deleteSingleFile(String docId) throws IOException;

@Override
public Set<FileInfoDTO> findFiles(@Nullable UUID checkInID) {
boolean isAdmin = currentUserServices.isAdmin();
validate(checkInID == null && !isAdmin, NOT_AUTHORIZED_MSG);

try {
Set<FileInfoDTO> result = new HashSet<>();
if (checkInID == null && isAdmin) {
getCheckinDocuments(result, Collections.emptySet());
} else if (checkInID != null) {
validate(!checkInServices.accessGranted(checkInID, currentUserServices.getCurrentUser().getId()),
"You are not authorized to perform this operation");

// If there aren't any documents, do not call
// getCheckinDocument. It assumes that an empty set means
// that it should attempt to get all documents. And, in this
// case, we just want an empty result set.
Set<CheckinDocument> checkinDocuments = checkinDocumentServices.read(checkInID);
if (!checkinDocuments.isEmpty()) {
getCheckinDocuments(result, checkinDocuments);
}
}

return result;
} catch (IOException e) {
LOG.error("Error occurred while retrieving files.", e);
throw new FileRetrievalException(e.getMessage());
}
}

@Override
public java.io.File downloadFiles(@NotNull String uploadDocId) {
MemberProfile currentUser = currentUserServices.getCurrentUser();
boolean isAdmin = currentUserServices.isAdmin();

CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId);
validate(cd == null, String.format("Unable to find record with id %s", uploadDocId));

CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId());

if(!isAdmin) {
validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG);
}
try {
java.io.File file = java.io.File.createTempFile("tmp", ".txt");
file.deleteOnExit();
try(
FileOutputStream myWriter = new FileOutputStream(file)
) {
downloadSingleFile(uploadDocId, myWriter);
return file;
} catch (IOException e) {
LOG.error("Error occurred while retrieving files.", e);
throw new FileRetrievalException(e.getMessage());
}
} catch(IOException e) {
LOG.error("Error occurred while attempting to create a temporary file.", e);
throw new FileRetrievalException(e.getMessage());
}
}

@Override
public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpload file) {
MemberProfile currentUser = currentUserServices.getCurrentUser();
boolean isAdmin = currentUserServices.isAdmin();
validate((file.getFilename() == null || file.getFilename().equals("")), "Please select a valid file before uploading.");

CheckIn checkIn = checkInServices.read(checkInID);
validate(checkIn == null, "Unable to find checkin record with id %s", checkInID);
if(!isAdmin) {
validate((!currentUser.getId().equals(checkIn.getTeamMemberId()) && !currentUser.getId().equals(checkIn.getPdlId())), "You are not authorized to perform this operation");
validate(checkIn.isCompleted(), NOT_AUTHORIZED_MSG);
}

// create folder for each team member
final String directoryName = MemberProfileUtils.getFullName(memberProfileServices.getById(checkIn.getTeamMemberId()));

try {
return uploadSingleFile(file, directoryName,
(fileId) -> {
//create record in checkin-document service
CheckinDocument cd = new CheckinDocument(checkInID, fileId);
checkinDocumentServices.save(cd);
return cd;
});
} catch (IOException e) {
LOG.error("Unexpected error processing file upload.", e);
throw new FileRetrievalException(e.getMessage());
}
}

@Override
public boolean deleteFile(@NotNull String uploadDocId) {
MemberProfile currentUser = currentUserServices.getCurrentUser();
boolean isAdmin = currentUserServices.isAdmin();

CheckinDocument cd = checkinDocumentServices.getFindByUploadDocId(uploadDocId);
validate(cd == null, String.format("Unable to find record with id %s", uploadDocId));

CheckIn associatedCheckin = checkInServices.read(cd.getCheckinsId());
if(!isAdmin) {
validate((!currentUser.getId().equals(associatedCheckin.getTeamMemberId()) && !currentUser.getId().equals(associatedCheckin.getPdlId())), NOT_AUTHORIZED_MSG);
}

try {
deleteSingleFile(uploadDocId);
checkinDocumentServices.deleteByUploadDocId(uploadDocId);
return true;
} catch (IOException e) {
LOG.error("Error occurred while deleting files.", e);
throw new FileRetrievalException(e.getMessage());
}
}

protected void validate(boolean isError, String message, Object... args) {
if(isError) {
throw new FileRetrievalException(String.format(message, args));
}
}
}
Loading

0 comments on commit e9b030d

Please sign in to comment.