From 914eb6f0c2a374c250622466fc17ba16e5f14c1c Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Thu, 29 Apr 2021 16:11:40 +0530 Subject: [PATCH 1/3] Initial version that creates and gets notifications --- .../com/appsmith/server/constants/Url.java | 1 + .../controllers/NotificationController.java | 21 +++++++ .../com/appsmith/server/domains/Comment.java | 6 ++ .../server/domains/CommentNotification.java | 12 ++++ .../appsmith/server/domains/Notification.java | 24 ++++++++ .../appsmith/server/helpers/PolicyUtils.java | 16 +++++ .../CustomNotificationRepository.java | 6 ++ .../CustomNotificationRepositoryImpl.java | 13 ++++ .../repositories/NotificationRepository.java | 12 ++++ .../server/services/CommentServiceImpl.java | 26 +++++++- .../server/services/NotificationService.java | 7 +++ .../services/NotificationServiceImpl.java | 61 +++++++++++++++++++ 12 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java index 7d4d23540d..87d8446484 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java @@ -29,4 +29,5 @@ public interface Url { String MARKETPLACE_ITEM_URL = BASE_URL + VERSION + "/items"; String ASSET_URL = BASE_URL + VERSION + "/assets"; String COMMENT_URL = BASE_URL + VERSION + "/comments"; + String NOTIFICATION_URL = BASE_URL + VERSION + "/notifications"; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java new file mode 100644 index 0000000000..b7a00dec57 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java @@ -0,0 +1,21 @@ +package com.appsmith.server.controllers; + +import com.appsmith.server.constants.Url; +import com.appsmith.server.domains.Notification; +import com.appsmith.server.services.NotificationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping(Url.NOTIFICATION_URL) +public class NotificationController extends BaseController { + + @Autowired + public NotificationController(NotificationService service) { + super(service); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java index dc67d106f4..541b026424 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java @@ -32,6 +32,12 @@ public class Comment extends BaseDomain { Body body; + /** + * Indicates whether this comment is the leading comment in it's thread. Such a comment cannot be deleted. + */ + @JsonIgnore + Boolean leading; + @Data public static class Body { List blocks; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java new file mode 100644 index 0000000000..0f68abbadc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java @@ -0,0 +1,12 @@ +package com.appsmith.server.domains; + +import lombok.Data; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +@Data +public class CommentNotification extends Notification { + + Comment comment; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java new file mode 100644 index 0000000000..b972eda2b6 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java @@ -0,0 +1,24 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.BaseDomain; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@EqualsAndHashCode(callSuper = true) +@Document +public class Notification extends BaseDomain { + + // TODO: This class extends BaseDomain, so it has policies. Should we use information from policies instead of this field? + String forUsername; + + /** + * Read status for this notification. If it is `true`, then this notification is read. If `false` or `null`, it's unread. + */ + Boolean isRead; + + public void cleanForClient() { + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java index b67a6d967a..7c88e95432 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java @@ -13,11 +13,13 @@ import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.NewActionRepository; import com.appsmith.server.repositories.NewPageRepository; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -282,4 +284,18 @@ public class PolicyUtils { return false; } + + public Set findUsernamesWithPermission(Set policies, AclPermission permission) { + if (CollectionUtils.isNotEmpty(policies) && permission != null) { + final String permissionString = permission.getValue(); + for (Policy policy : policies) { + if (permissionString.equals(policy.getPermission())) { + return policy.getUsers(); + } + } + } + + return Collections.emptySet(); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java new file mode 100644 index 0000000000..9f420f5cf7 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java @@ -0,0 +1,6 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; + +public interface CustomNotificationRepository extends AppsmithRepository { +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java new file mode 100644 index 0000000000..6c4b50c95c --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java @@ -0,0 +1,13 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MongoConverter; + +public class CustomNotificationRepositoryImpl extends BaseAppsmithRepositoryImpl implements CustomNotificationRepository { + + public CustomNotificationRepositoryImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter) { + super(mongoOperations, mongoConverter); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java new file mode 100644 index 0000000000..eb81b59bb5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface NotificationRepository extends BaseRepository, CustomNotificationRepository { + + Flux findByForUsername(String userId); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java index 17f372b4e4..a1dd4fe781 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java @@ -5,10 +5,13 @@ import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Comment; +import com.appsmith.server.domains.CommentNotification; import com.appsmith.server.domains.CommentThread; +import com.appsmith.server.domains.Notification; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.repositories.CommentRepository; import com.appsmith.server.repositories.CommentThreadRepository; import lombok.extern.slf4j.Slf4j; @@ -41,8 +44,10 @@ public class CommentServiceImpl extends BaseService { + final Set usernames = policyUtils.findUsernamesWithPermission( + savedComment.getPolicies(), AclPermission.READ_COMMENT); + + List> monos = new ArrayList<>(); + for (String username : usernames) { + final CommentNotification notification = new CommentNotification(); + notification.setComment(savedComment); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } + + return Flux.concat(monos).then(Mono.just(savedComment)); }); } @@ -137,6 +160,7 @@ public class CommentServiceImpl extends BaseService> commentSaverMonos = new ArrayList<>(); if (!CollectionUtils.isEmpty(thread.getComments())) { + thread.getComments().get(0).setLeading(true); for (final Comment comment : thread.getComments()) { comment.setId(null); commentSaverMonos.add(create(thread.getId(), comment)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java new file mode 100644 index 0000000000..cce58ac380 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java @@ -0,0 +1,7 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Notification; + +public interface NotificationService extends CrudService { + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java new file mode 100644 index 0000000000..614e68fddc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java @@ -0,0 +1,61 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Notification; +import com.appsmith.server.repositories.NotificationRepository; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import javax.validation.Validator; + +@Slf4j +@Service +public class NotificationServiceImpl + extends BaseService + implements NotificationService { + + private final SessionUserService sessionUserService; + + public NotificationServiceImpl( + Scheduler scheduler, + Validator validator, + MongoConverter mongoConverter, + ReactiveMongoTemplate reactiveMongoTemplate, + NotificationRepository repository, + AnalyticsService analyticsService, + SessionUserService sessionUserService + ) { + super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); + this.sessionUserService = sessionUserService; + } + + @Override + public Mono create(Notification notification) { + Mono notificationWithUsernameMono; + if (StringUtils.isEmpty(notification.getForUsername())) { + notificationWithUsernameMono = sessionUserService.getCurrentUser() + .map(user -> { + notification.setForUsername(user.getUsername()); + return notification; + }); + } else { + notificationWithUsernameMono = Mono.just(notification); + } + + return notificationWithUsernameMono + .flatMap(super::create); + } + + @Override + public Flux get(MultiValueMap params) { + return sessionUserService.getCurrentUser() + .flatMapMany(user -> repository.findByForUsername(user.getUsername())); + } + +} From 75479448ccf95e0cfe6fb8bb35f5b673c1bfc482 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 18 May 2021 14:51:31 +0530 Subject: [PATCH 2/3] Watch notification collection in RTS --- app/rts/src/server.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/rts/src/server.ts b/app/rts/src/server.ts index 5e627d7298..27c772250c 100644 --- a/app/rts/src/server.ts +++ b/app/rts/src/server.ts @@ -233,6 +233,34 @@ async function watchMongoDB(io) { } }) + const notificationsStream = db.collection("notification").watch( + [ + // Prevent server-internal fields from being sent to the client. + { + $unset: [ + "deletedAt", + "deleted", + "_class", + ].map(f => "fullDocument." + f) + }, + ], + { fullDocument: "updateLookup" } + ); + + notificationsStream.on("change", async (event: mongodb.ChangeEventCR) => { + console.log("notification event", event) + const notification = event.fullDocument + + if (notification == null) { + // This happens when `event.operationType === "drop"`, when a notification is deleted. + console.error("Null document recieved for notification change event", event) + return + } + + const eventName = event.operationType + ":" + event.ns.coll + io.to("email:" + notification.forUsername).emit(eventName, { notification }) + }) + process.on("exit", () => { (commentChangeStream != null ? commentChangeStream.close() : Promise.bind(client).resolve()) .then(client.close.bind(client)) From 28cab61e5b5683e2514f7f868105944159ceda37 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 18 May 2021 17:16:09 +0530 Subject: [PATCH 3/3] Separate notifications for comment threads --- .../server/domains/CommentNotification.java | 4 +- .../domains/CommentThreadNotification.java | 14 +++++ .../appsmith/server/domains/Notification.java | 3 +- .../server/services/CommentServiceImpl.java | 51 +++++++++++++++---- 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java index 0f68abbadc..b2277fd173 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java @@ -1,10 +1,12 @@ package com.appsmith.server.domains; import lombok.Data; +import lombok.EqualsAndHashCode; import org.springframework.data.mongodb.core.mapping.Document; -@Document @Data +@EqualsAndHashCode(callSuper = true) +@Document public class CommentNotification extends Notification { Comment comment; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java new file mode 100644 index 0000000000..ab3834805c --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java @@ -0,0 +1,14 @@ +package com.appsmith.server.domains; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@EqualsAndHashCode(callSuper = true) +@Document +public class CommentThreadNotification extends Notification { + + CommentThread commentThread; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java index b972eda2b6..79ffee5af1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java @@ -18,7 +18,8 @@ public class Notification extends BaseDomain { */ Boolean isRead; - public void cleanForClient() { + public String getType() { + return getClass().getSimpleName(); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java index b1b1b4b50d..1935b125b0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java @@ -8,6 +8,7 @@ import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Comment; import com.appsmith.server.domains.CommentNotification; import com.appsmith.server.domains.CommentThread; +import com.appsmith.server.domains.CommentThreadNotification; import com.appsmith.server.domains.Notification; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; @@ -79,6 +80,10 @@ public class CommentServiceImpl extends BaseService create(String threadId, Comment comment) { + return create(threadId, comment, true); + } + + public Mono create(String threadId, Comment comment, boolean shouldCreateNotification) { if (StringUtils.isWhitespace(comment.getAuthorName())) { // Error: User can't explicitly set the author name. It will be the currently logged in user. return Mono.empty(); @@ -117,18 +122,26 @@ public class CommentServiceImpl extends BaseService { + .flatMap(tuple -> { + final User user = tuple.getT1(); + final Comment savedComment = tuple.getT2(); + final Set usernames = policyUtils.findUsernamesWithPermission( savedComment.getPolicies(), AclPermission.READ_COMMENT); List> monos = new ArrayList<>(); for (String username : usernames) { - final CommentNotification notification = new CommentNotification(); - notification.setComment(savedComment); - notification.setForUsername(username); - monos.add(notificationService.create(notification)); + if (!username.equals(user.getUsername())) { + final CommentNotification notification = new CommentNotification(); + notification.setComment(savedComment); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } } return Flux.concat(monos).then(Mono.just(savedComment)); @@ -190,9 +203,11 @@ public class CommentServiceImpl extends BaseService { + .zipWith(sessionUserService.getCurrentUser()) + .flatMap(tuple -> { + final List comments = tuple.getT1(); + final User user = tuple.getT2(); + commentThread.setComments(comments); commentThread.setIsViewed(true); - return commentThread; + + final Set usernames = policyUtils.findUsernamesWithPermission( + commentThread.getPolicies(), AclPermission.READ_THREAD); + + List> monos = new ArrayList<>(); + for (String username : usernames) { + if (!username.equals(user.getUsername())) { + final CommentThreadNotification notification = new CommentThreadNotification(); + notification.setCommentThread(commentThread); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } + } + + return Flux.concat(monos).then(Mono.just(commentThread)); }); }