Add a bot thread when someone adds first comment or tags someone is the comment for the first time (#5521)

* resolved conflicts

* -fixed some issues in PR

* -used sequence in generating thread number

* -refactored the create thread method

* -refactored comment service

* -add bot reply to the first comment thread of an user

* -added a new bot thread when user resolves the first bot thread

* -handle the case when user data can be absent

* -add organization id and widget type to comment and comment threads

* -resolved conflicts

* -mark appsmith bot username as a tagged user in the bot comment

* -turn a private thread to a public thread when someone is tagged in it

* -updated as per the review comments

* -fixed compile error in unit test

* -updated as per PR review comments

* -removed commented code
This commit is contained in:
Nayan 2021-07-15 23:45:08 +06:00 committed by GitHub
parent 56caba8f55
commit 8fcbb6e4c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 692 additions and 256 deletions

View File

@ -22,4 +22,7 @@ public class EmailConfig {
@Value("${emails.welcome.enabled:true}")
private boolean isWelcomeEmailEnabled;
@Value("${mail.support}")
private String supportEmailAddress;
}

View File

@ -1,6 +1,5 @@
package com.appsmith.server.constants;
public class Appsmith {
public final static String APPSMITH_REGISTERED = "appsmith_registered";
}

View File

@ -0,0 +1,5 @@
package com.appsmith.server.constants;
public enum CommentBotEvent {
COMMENTED, RESOLVED
}

View File

@ -0,0 +1,6 @@
package com.appsmith.server.constants;
public class CommentConstants {
public final static String APPSMITH_BOT_NAME = "Appsmith Bot";
public final static String APPSMITH_BOT_USERNAME = "appsmith";
}

View File

@ -0,0 +1,21 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractCommentDomain extends BaseDomain {
String pageId;
String applicationId;
String applicationName;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorName; // Display name of the user, who authored this comment or thread.
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorUsername; // username i.e. email of the user, who authored this comment or thread.
String orgId;
}

View File

@ -1,11 +1,11 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
@ -17,7 +17,7 @@ import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER;
@Data
@EqualsAndHashCode(callSuper = false)
@Document
public class Comment extends BaseDomain {
public class Comment extends AbstractCommentDomain {
String threadId;
@ -27,19 +27,6 @@ public class Comment extends BaseDomain {
@JsonIgnore
String authorId;
/**
* Display name of the user, who authored this comment.
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorName;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorUsername;
private String applicationId;
private String applicationName;
private String pageId;
Body body;
/** Edit/Published Mode */
@ -70,6 +57,8 @@ public class Comment extends BaseDomain {
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Range {
Integer offset;
Integer length;
@ -84,16 +73,22 @@ public class Comment extends BaseDomain {
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class EntityData {
Mention mention;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Mention {
String name;
EntityUser user;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class EntityUser {
String username;
String roleName;

View File

@ -1,8 +1,6 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.annotation.Transient;
@ -17,7 +15,9 @@ import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER;
@Data
@EqualsAndHashCode(callSuper = false)
@Document
public class CommentThread extends BaseDomain {
public class CommentThread extends AbstractCommentDomain {
Boolean isPrivate;
String tabId;
@ -25,7 +25,7 @@ public class CommentThread extends BaseDomain {
String refId;
String pageId;
String widgetType;
CommentThreadState pinnedState;
@ -33,10 +33,6 @@ public class CommentThread extends BaseDomain {
String sequenceId;
String applicationId;
String applicationName;
@JsonIgnore
Set<String> viewedByUsers;
@ -49,15 +45,6 @@ public class CommentThread extends BaseDomain {
/** Edit/Published Mode */
String mode;
/**
* Display name of the user, who authored this comment thread.
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorName;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String authorUsername;
@Transient
Boolean isViewed;

View File

@ -1,6 +1,7 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.constants.CommentBotEvent;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.NoArgsConstructor;
@ -32,6 +33,9 @@ public class UserData extends BaseDomain {
// list of organisation ids that were recently accessed by the user
private List<String> recentlyUsedOrgIds;
// last event triggered by comment bot for this user
private CommentBotEvent latestCommentEvent;
public UserData(String userId) {
this.userId = userId;
}

View File

@ -1,6 +1,8 @@
package com.appsmith.server.helpers;
import com.appsmith.server.constants.CommentConstants;
import com.appsmith.server.domains.Comment;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashSet;
@ -22,8 +24,32 @@ public class CommentUtils {
&& commentEntity.getType().equals("mention")) {
// this comment has a mention, check the provided user is mentioned or not
if(commentEntity.getData() != null) {
Comment.EntityData.Mention mention = commentEntity.getData().getMention();
if(mention.getUser().getUsername().equals(userEmail)) {
String mentionedUsername = getMentionedUsername(commentEntity.getData().getMention());
if(userEmail.equals(mentionedUsername)) {
return true;
}
}
}
}
}
return false;
}
/**
* Checks if anyone except the bot is mentioned in this comment
* @param comment Comment
* @return true if comment has someone in mention, false otherwise
*/
public static boolean isAnyoneMentioned(Comment comment) {
if(comment.getBody() != null && comment.getBody().getEntityMap() != null) {
for(String key : comment.getBody().getEntityMap().keySet()) {
Comment.Entity commentEntity = comment.getBody().getEntityMap().get(key);
if(commentEntity != null && commentEntity.getType() != null
&& commentEntity.getType().equals("mention")) {
// this comment has a mention, check the provided user is mentioned or not
if(commentEntity.getData() != null) {
String mentionedUsername = getMentionedUsername(commentEntity.getData().getMention());
if(!StringUtils.isEmpty(mentionedUsername) && !CommentConstants.APPSMITH_BOT_USERNAME.equals(mentionedUsername)) {
return true;
}
}
@ -53,8 +79,10 @@ public class CommentUtils {
&& commentEntity.getType().equals("mention")) {
// this comment has a mention, check the provided user is mentioned or not
if(commentEntity.getData() != null) {
Comment.EntityData.Mention mention = commentEntity.getData().getMention();
usernamesSet.add(mention.getUser().getUsername());
String mentionedUsername = getMentionedUsername(commentEntity.getData().getMention());
if(!StringUtils.isEmpty(mentionedUsername) && !mentionedUsername.equals(CommentConstants.APPSMITH_BOT_USERNAME)) {
usernamesSet.add(mentionedUsername);
}
}
}
}
@ -71,4 +99,12 @@ public class CommentUtils {
}
return commentLines;
}
private static String getMentionedUsername(Comment.EntityData.Mention mention){
if(mention.getUser() != null) {
return mention.getUser().getUsername();
} else {
return mention.getName();
}
}
}

View File

@ -15,7 +15,6 @@ import com.appsmith.server.repositories.CommentThreadRepository;
import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.solutions.UserChangedHandler;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
@ -44,7 +43,6 @@ public class PolicyUtils {
private final DatasourceRepository datasourceRepository;
private final NewPageRepository newPageRepository;
private final NewActionRepository newActionRepository;
private final UserChangedHandler userChangedHandler;
private final CommentThreadRepository commentThreadRepository;
public <T extends BaseDomain> T addPoliciesToExistingObject(Map<String, Policy> policyMap, T obj) {
@ -110,13 +108,17 @@ public class PolicyUtils {
* @return
*/
public Map<String, Policy> generatePolicyFromPermission(Set<AclPermission> permissions, User user) {
return generatePolicyFromPermission(permissions, user.getUsername());
}
public Map<String, Policy> generatePolicyFromPermission(Set<AclPermission> permissions, String username) {
return permissions.stream()
.map(perm -> {
// Create a policy for the invited user using the permission as per the role
Policy policyWithCurrentPermission = Policy.builder().permission(perm.getValue())
.users(Set.of(user.getUsername())).build();
.users(Set.of(username)).build();
// Generate any and all lateral policies that might come with the current permission
Set<Policy> policiesForUser = policyGenerator.getLateralPolicies(perm, Set.of(user.getUsername()), null);
Set<Policy> policiesForUser = policyGenerator.getLateralPolicies(perm, Set.of(username), null);
policiesForUser.add(policyWithCurrentPermission);
return policiesForUser;
})
@ -219,21 +221,25 @@ public class PolicyUtils {
.saveAll(updatedPages));
}
public Flux<CommentThread> updateWithApplicationPermissionsToAllItsCommentThreads(String applicationId, Map<String, Policy> commentThreadPolicyMap, boolean addPolicyToObject) {
public Flux<CommentThread> updateWithApplicationPermissionsToAllItsCommentThreads(
String applicationId, Map<String, Policy> commentThreadPolicyMap, boolean addPolicyToObject) {
return
// fetch comment threads with read permissions
commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREAD)
.switchIfEmpty(Mono.empty())
.map(thread -> {
if (addPolicyToObject) {
return addPoliciesToExistingObject(commentThreadPolicyMap, thread);
} else {
return removePoliciesFromExistingObject(commentThreadPolicyMap, thread);
if(!Boolean.TRUE.equals(thread.getIsPrivate())) {
if (addPolicyToObject) {
return addPoliciesToExistingObject(commentThreadPolicyMap, thread);
} else {
return removePoliciesFromExistingObject(commentThreadPolicyMap, thread);
}
}
return thread;
})
.collectList()
.flatMapMany(commentThreads -> commentThreadRepository.saveAll(commentThreads));
.flatMapMany(commentThreadRepository::saveAll);
}
/**

View File

@ -0,0 +1,28 @@
package com.appsmith.server.helpers;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
public class TemplateUtils {
/**
* This function replaces the variables in an email template to actual values. It uses the Mustache SDK.
*
* @param template The name of the template where the HTML text can be found
* @param params A Map of key-value pairs with the key being the variable in the template & value being the actual
* value with which it must be replaced.
* @return Template string with Mustache replacements applied.
* @throws IOException bubbled from Mustache renderer.
*/
public static String parseTemplate(String template, Map<String, ? extends Object> params) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
StringWriter stringWriter = new StringWriter();
Mustache mustache = mf.compile(template);
mustache.execute(stringWriter, params).flush();
return stringWriter.toString();
}
}

View File

@ -1,9 +1,7 @@
package com.appsmith.server.notifications;
import com.appsmith.server.configurations.EmailConfig;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.appsmith.server.helpers.TemplateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
@ -17,7 +15,6 @@ import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Map;
@ -54,7 +51,7 @@ public class EmailSender {
*/
Mono.fromCallable(() -> {
try {
return replaceEmailTemplate(text, params);
return TemplateUtils.parseTemplate(text, params);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
@ -113,23 +110,6 @@ public class EmailSender {
}
}
/**
* This function replaces the variables in an email template to actual values. It uses the Mustache SDK.
*
* @param template The name of the template where the HTML text can be found
* @param params A Map of key-value pairs with the key being the variable in the template & value being the actual
* value with which it must be replaced.
* @return Template string with Mustache replacements applied.
* @throws IOException bubbled from Mustache renderer.
*/
private String replaceEmailTemplate(String template, Map<String, ? extends Object> params) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
StringWriter stringWriter = new StringWriter();
Mustache mustache = mf.compile(template);
mustache.execute(stringWriter, params).flush();
return stringWriter.toString();
}
private InternetAddress makeFromAddress() {
try {
return new InternetAddress(this.emailConfig.getMailFrom(), "Appsmith");

View File

@ -11,5 +11,6 @@ import java.util.Set;
public interface CustomCommentThreadRepository extends AppsmithRepository<CommentThread> {
Flux<CommentThread> findByApplicationId(String applicationId, AclPermission permission);
Mono<UpdateResult> addToSubscribers(String threadId, Set<String> usernames);
Mono<CommentThread> findPrivateThread(String applicationId);
Mono<Long> countUnreadThreads(String applicationId, String userEmail);
}

View File

@ -17,6 +17,7 @@ import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Set;
import static java.lang.Boolean.TRUE;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Component
@ -47,6 +48,15 @@ public class CustomCommentThreadRepositoryImpl extends BaseAppsmithRepositoryImp
);
}
@Override
public Mono<CommentThread> findPrivateThread(String applicationId) {
List<Criteria> criteria = List.of(
where(fieldName(QCommentThread.commentThread.applicationId)).is(applicationId),
where(fieldName(QCommentThread.commentThread.isPrivate)).is(TRUE)
);
return queryOne(criteria, AclPermission.READ_THREAD);
}
@Override
public Mono<Long> countUnreadThreads(String applicationId, String userEmail) {
List<Criteria> criteriaList = List.of(

View File

@ -3,34 +3,39 @@ package com.appsmith.server.services;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.constants.CommentBotEvent;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.Notification;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserData;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.CommentUtils;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.helpers.TemplateUtils;
import com.appsmith.server.repositories.CommentRepository;
import com.appsmith.server.repositories.CommentThreadRepository;
import com.appsmith.server.repositories.UserDataRepository;
import com.appsmith.server.solutions.EmailEventHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import javax.validation.Validator;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@ -39,11 +44,20 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.appsmith.server.constants.CommentConstants.APPSMITH_BOT_NAME;
import static com.appsmith.server.constants.CommentConstants.APPSMITH_BOT_USERNAME;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@Slf4j
@Service
public class CommentServiceImpl extends BaseService<CommentRepository, Comment, String> implements CommentService {
private static final String HOW_TO_TAG_USER_COMMENT = "bot/howToTagUser.html";
private static final String HOW_TO_TAG_BOT_COMMENT = "bot/howToTagBot.html";
private final CommentThreadRepository threadRepository;
private final UserDataRepository userDataRepository;
private final UserService userService;
private final SessionUserService sessionUserService;
@ -53,6 +67,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
private final PolicyGenerator policyGenerator;
private final PolicyUtils policyUtils;
private final EmailEventHandler emailEventHandler;
private final SequenceService sequenceService;
public CommentServiceImpl(
Scheduler scheduler,
@ -68,7 +83,8 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
NotificationService notificationService,
PolicyGenerator policyGenerator,
PolicyUtils policyUtils,
EmailEventHandler emailEventHandler) {
EmailEventHandler emailEventHandler,
UserDataRepository userDataRepository, SequenceService sequenceService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.threadRepository = threadRepository;
this.userService = userService;
@ -78,102 +94,137 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
this.policyGenerator = policyGenerator;
this.policyUtils = policyUtils;
this.emailEventHandler = emailEventHandler;
this.userDataRepository = userDataRepository;
this.sequenceService = sequenceService;
}
@Override
public Mono<Comment> create(String threadId, Comment comment, String originHeader) {
return create(threadId, comment, originHeader, true);
}
private Mono<Comment> create(String threadId, Comment comment, String originHeader, 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();
}
final Mono<User> userMono = sessionUserService.getCurrentUser()
.flatMap(user -> {
if (user.getId() == null) {
return userService.findByEmail(user.getEmail());
} else {
return Mono.just(user);
final Mono<User> userMono = sessionUserService.getCurrentUser().flatMap(user -> {
if (user.getId() == null) {
return userService.findByEmail(user.getEmail());
} else {
return Mono.just(user);
}
});
Mono<CommentThread> commentThreadMono = threadRepository.findById(threadId, AclPermission.COMMENT_ON_THREAD)
.flatMap(commentThread -> {
if (CommentUtils.isAnyoneMentioned(comment) && Boolean.TRUE.equals(commentThread.getIsPrivate())) {
return convertToPublic(commentThread);
}
return Mono.just(commentThread);
});
final Mono<CommentThread> threadMono = threadRepository.findById(threadId, AclPermission.COMMENT_ON_THREAD);
return Mono.zip(userMono, threadMono)
return userMono.zipWith(commentThreadMono)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, "comment thread", threadId)))
.flatMap(tuple -> {
final User user = tuple.getT1();
final CommentThread thread = tuple.getT2();
comment.setAuthorId(user.getId());
comment.setThreadId(threadId);
comment.setApplicationId(thread.getApplicationId());
comment.setApplicationName(thread.getApplicationName());
comment.setPageId(thread.getPageId());
final Set<Policy> policies = policyGenerator.getAllChildPolicies(
thread.getPolicies(),
CommentThread.class,
Comment.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_COMMENT),
user
).get(AclPermission.MANAGE_COMMENT.getValue()));
comment.setPolicies(policies);
String authorName = user.getName() != null ? user.getName(): user.getUsername();
comment.setAuthorUsername(user.getUsername());
comment.setAuthorName(authorName);
Set<String> subscribersFromThisComment = CommentUtils.getSubscriberUsernames(comment);
// add them to current thread so that we don't need to query again
if(thread.getSubscribers() != null) {
thread.getSubscribers().addAll(subscribersFromThisComment);
} else {
thread.setSubscribers(subscribersFromThisComment);
}
return Mono.zip(
Mono.just(user),
Mono.just(thread),
repository.save(comment),
threadRepository.addToSubscribers(threadId, subscribersFromThisComment)
);
})
.flatMap(tuple -> {
final User user = tuple.getT1();
CommentThread commentThread = tuple.getT2();
final Comment savedComment = tuple.getT3();
Mono<Boolean> publishEmailMono = emailEventHandler.publish(
comment.getAuthorUsername(),
commentThread.getApplicationId(),
comment,
originHeader,
commentThread.getSubscribers()
);
if (shouldCreateNotification) {
final Set<String> usernames = commentThread.getSubscribers();
List<Mono<Notification>> notificationMonos = new ArrayList<>();
for (String username : usernames) {
if (!username.equals(user.getUsername())) {
Mono<Notification> notificationMono = notificationService.createNotification(
savedComment, username
);
notificationMonos.add(notificationMono);
}
}
return Flux.concat(notificationMonos).then(publishEmailMono).thenReturn(savedComment);
} else {
return publishEmailMono.thenReturn(savedComment);
}
return create(thread, user, comment, originHeader, true);
});
}
/**
* Converts a private bot thread to a public thread.
* It sets the isPrivate flag to false, changes the sequence and updates the policy
* @param commentThread
* @return
*/
private Mono<CommentThread> convertToPublic(CommentThread commentThread) {
return applicationService.findById(commentThread.getApplicationId())
.zipWith(sequenceService.getNext(CommentThread.class, commentThread.getApplicationId()))
.flatMap(objects -> {
Application application = objects.getT1();
commentThread.setSequenceId("#" + objects.getT2());
commentThread.setIsPrivate(FALSE);
final Set<Policy> policies = new HashSet<>();
policies.addAll(policyGenerator.getAllChildPolicies(
application.getPolicies(),
Application.class,
CommentThread.class
));
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD),
commentThread.getAuthorUsername()
).get(AclPermission.MANAGE_THREAD.getValue()));
commentThread.setPolicies(policies);
return threadRepository.save(commentThread);
});
}
private Mono<Comment> create(CommentThread commentThread, User user, Comment comment, String originHeader, boolean shouldCreateNotification) {
comment.setAuthorId(user.getId());
comment.setThreadId(commentThread.getId());
comment.setApplicationId(commentThread.getApplicationId());
comment.setApplicationName(commentThread.getApplicationName());
comment.setPageId(commentThread.getPageId());
comment.setOrgId(commentThread.getOrgId());
final Set<Policy> policies = policyGenerator.getAllChildPolicies(
commentThread.getPolicies(),
CommentThread.class,
Comment.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_COMMENT),
user
).get(AclPermission.MANAGE_COMMENT.getValue()));
comment.setPolicies(policies);
String authorName = user.getName() != null ? user.getName() : user.getUsername();
comment.setAuthorUsername(user.getUsername());
comment.setAuthorName(authorName);
Mono<Comment> commentMono;
if (!TRUE.equals(commentThread.getIsPrivate())) {
Set<String> subscribersFromThisComment = CommentUtils.getSubscriberUsernames(comment);
// add them to current thread so that we don't need to query again
if (commentThread.getSubscribers() != null) {
commentThread.getSubscribers().addAll(subscribersFromThisComment);
} else {
commentThread.setSubscribers(subscribersFromThisComment);
}
commentMono = threadRepository.addToSubscribers(commentThread.getId(), subscribersFromThisComment)
.then(repository.save(comment));
} else {
commentMono = repository.save(comment);
}
return commentMono.flatMap(savedComment -> {
boolean isPrivateThread = TRUE.equals(commentThread.getIsPrivate());
Mono<Boolean> publishEmail = emailEventHandler.publish(
comment.getAuthorUsername(),
commentThread.getApplicationId(),
comment,
originHeader,
commentThread.getSubscribers()
);
if (shouldCreateNotification && !isPrivateThread) {
final Set<String> usernames = commentThread.getSubscribers();
List<Mono<Notification>> notificationMonos = new ArrayList<>();
for (String username : usernames) {
if (!username.equals(user.getUsername()) && !username.equals(APPSMITH_BOT_USERNAME)) {
Mono<Notification> notificationMono = notificationService.createNotification(
savedComment, username
);
notificationMonos.add(notificationMono);
}
}
return publishEmail.then(Flux.merge(notificationMonos).then(Mono.just(savedComment)));
} else {
return publishEmail.thenReturn(savedComment);
}
});
}
@Override
public Mono<CommentThread> createThread(CommentThread commentThread, String originHeader) {
// 1. Check if this user has permission on the application given by `commentThread.applicationId`.
@ -181,79 +232,61 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
// 3. Pull the comment out of the list of comments, set it's `threadId` and save it separately.
// 4. Populate the new comment's ID into the CommentThread object sent as response.
final String applicationId = commentThread.getApplicationId();
CommentThread.CommentThreadState initState = new CommentThread.CommentThreadState();
initState.setActive(false);
initState.setAuthorName("");
initState.setAuthorUsername("");
commentThread.setPinnedState(initState);
commentThread.setResolvedState(initState);
//TODO : Use sequenceDB for optimised results here
Query query = new Query();
query.addCriteria(Criteria.where("applicationId").is(applicationId));
return mongoTemplate
.count(query, CommentThread.class)
.flatMap(count -> {
count += 1;
commentThread.setSequenceId("#" + count);
return Mono.zip(
sessionUserService.getCurrentUser(),
applicationService.findById(applicationId, AclPermission.COMMENT_ON_APPLICATIONS)
);
})
.switchIfEmpty(Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId)
))
.flatMap(tuple -> {
final User user = tuple.getT1();
final Application application = tuple.getT2();
commentThread.setApplicationName(application.getName());
commentThread.setAuthorName(user.getName());
commentThread.setAuthorUsername(user.getUsername());
final Set<Policy> policies = policyGenerator.getAllChildPolicies(
application.getPolicies(),
Application.class,
CommentThread.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD),
user
).get(AclPermission.MANAGE_THREAD.getValue()));
commentThread.setPolicies(policies);
Set<String> viewedUser = new HashSet<>();
viewedUser.add(user.getUsername());
commentThread.setViewedByUsers(viewedUser);
return threadRepository.save(commentThread);
})
.flatMapMany(thread -> {
List<Mono<Comment>> commentSaverMonos = new ArrayList<>();
if (!CollectionUtils.isEmpty(thread.getComments())) {
thread.getComments().get(0).setLeading(true);
boolean isFirst = true;
for (final Comment comment : thread.getComments()) {
comment.setId(null);
commentSaverMonos.add(create(thread.getId(), comment, originHeader, !isFirst));
isFirst = false;
final Mono<User> userMono = sessionUserService.getCurrentUser().flatMap(user -> {
if (user.getId() == null) {
return userService.findByEmail(user.getEmail());
} else {
return Mono.just(user);
}
});
return userMono.flatMap(user -> {
return userDataRepository.findByUserId(user.getId())
.defaultIfEmpty(new UserData(user.getId()))
.zipWith(applicationService.findById(applicationId, AclPermission.COMMENT_ON_APPLICATIONS))
.switchIfEmpty(Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId)
))
.flatMap(tuple -> {
final UserData userData = tuple.getT1();
final Application application = tuple.getT2();
// check whether this thread should be converted to bot thread
if (userData.getLatestCommentEvent() == null) {
commentThread.setIsPrivate(true);
userData.setLatestCommentEvent(CommentBotEvent.COMMENTED);
return userDataRepository.save(userData).then(
saveCommentThread(commentThread, application, user)
);
}
}
return saveCommentThread(commentThread, application, user);
})
.flatMapMany(thread -> {
List<Mono<Comment>> commentSaverMonos = new ArrayList<>();
// Using `concat` here so that the comments are saved one after the other, so that their `createdAt`
// value is meaningful.
return Flux.concat(commentSaverMonos);
})
.collectList()
.zipWith(sessionUserService.getCurrentUser())
.map(tuple -> {
final List<Comment> comments = tuple.getT1();
commentThread.setComments(comments);
commentThread.setIsViewed(true);
return commentThread;
});
if (!CollectionUtils.isEmpty(thread.getComments())) {
thread.getComments().get(0).setLeading(true);
boolean isFirst = true;
for (final Comment comment : thread.getComments()) {
comment.setId(null);
commentSaverMonos.add(create(thread, user, comment, originHeader, !isFirst));
isFirst = false;
}
}
if (TRUE.equals(thread.getIsPrivate())) {
// this is the first thread by this user, add a bot comment also
commentSaverMonos.add(createBotComment(thread, user, CommentBotEvent.COMMENTED));
}
// Using `concat` here so that the comments are saved one after the other, so that their `createdAt`
// value is meaningful.
return Flux.concat(commentSaverMonos);
})
.collectList()
.map(commentList -> {
commentThread.setComments(commentList);
commentThread.setIsViewed(true);
return commentThread;
});
});
}
@Override
@ -264,17 +297,19 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
@Override
public Mono<CommentThread> updateThread(String threadId, CommentThread commentThread, String originHeader) {
return Mono.zip(
sessionUserService.getCurrentUser(),
// Resolving, pinning and marking as read don't need manage permission on the thread.
threadRepository.findById(threadId, AclPermission.READ_THREAD)
)
return sessionUserService.getCurrentUser().flatMap(user -> {
if (user.getId() == null) {
return userService.findByEmail(user.getEmail());
} else {
return Mono.just(user);
}
}).zipWith(threadRepository.findById(threadId, AclPermission.READ_THREAD))
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, "comment thread", threadId)))
.flatMap(tuple -> {
final User user = tuple.getT1();
final CommentThread threadFromDb = tuple.getT2();
String authorName = user.getName() != null ? user.getName(): user.getUsername();
String authorName = user.getName() != null ? user.getName() : user.getUsername();
if (commentThread.getResolvedState() != null) {
CommentThread.CommentThreadState state = new CommentThread.CommentThreadState();
@ -313,20 +348,58 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
updatedThread.setIsViewed(true);
// send email if comment thread is resolved
CommentThread.CommentThreadState resolvedState = commentThread.getResolvedState();
if(resolvedState != null && resolvedState.getActive()) {
return emailEventHandler.publish(
user.getUsername(),
updatedThread.getApplicationId(),
updatedThread,
originHeader
).thenReturn(updatedThread);
} else {
return Mono.just(updatedThread);
if (resolvedState != null && resolvedState.getActive()) {
if (Boolean.TRUE.equals(updatedThread.getIsPrivate())) {
return triggerBotThreadResolved(threadFromDb, user).thenReturn(updatedThread);
} else {
return emailEventHandler.publish(
user.getUsername(),
updatedThread.getApplicationId(),
updatedThread,
originHeader
).thenReturn(updatedThread);
}
}
return Mono.just(updatedThread);
});
});
}
private Mono<Boolean> triggerBotThreadResolved(CommentThread resolvedThread, User user) {
return userDataRepository.findByUserId(user.getId())
.defaultIfEmpty(new UserData(user.getId()))
.flatMap(userData -> {
if (userData.getLatestCommentEvent() == CommentBotEvent.COMMENTED) {
// update the user data
userData.setLatestCommentEvent(CommentBotEvent.RESOLVED);
Mono<UserData> saveUserDataMono = userDataRepository.save(userData);
Mono<CommentThread> saveThreadMono = applicationService.getById(resolvedThread.getApplicationId())
.flatMap(application -> {
// create a new bot thread
CommentThread commentThread = new CommentThread();
commentThread.setIsPrivate(true);
CommentThread.Position position = new CommentThread.Position();
position.setTop(0.558882236480713f);
position.setLeft(73.5241470336914f);
commentThread.setPosition(position);
commentThread.setPageId(resolvedThread.getPageId());
commentThread.setRefId(resolvedThread.getRefId());
commentThread.setMode(resolvedThread.getMode());
return saveCommentThread(commentThread, application, user)
.flatMap(savedCommentThread ->
createBotComment(savedCommentThread, user, CommentBotEvent.RESOLVED)
.thenReturn(savedCommentThread)
);
});
return saveUserDataMono.then(saveThreadMono).thenReturn(TRUE);
}
return Mono.just(FALSE);
});
}
@Override
public Mono<List<CommentThread>> getThreadsByApplicationId(String applicationId) {
return threadRepository.findByApplicationId(applicationId, AclPermission.READ_THREAD)
@ -342,7 +415,7 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
for (CommentThread thread : threads) {
thread.setComments(new LinkedList<>());
if(thread.getViewedByUsers() != null && thread.getViewedByUsers().contains(user.getUsername())) {
if (thread.getViewedByUsers() != null && thread.getViewedByUsers().contains(user.getUsername())) {
thread.setIsViewed(true);
} else {
thread.setIsViewed(false);
@ -414,6 +487,103 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
});
}
private Mono<CommentThread> saveCommentThread(CommentThread commentThread, Application application, User user) {
CommentThread.CommentThreadState initState = new CommentThread.CommentThreadState();
initState.setActive(false);
initState.setAuthorName("");
initState.setAuthorUsername("");
commentThread.setOrgId(application.getOrganizationId());
commentThread.setPinnedState(initState);
commentThread.setResolvedState(initState);
commentThread.setApplicationId(application.getId());
commentThread.setApplicationName(application.getName());
commentThread.setAuthorName(user.getName());
commentThread.setAuthorUsername(user.getUsername());
commentThread.setViewedByUsers(Set.of(user.getUsername()));
final Set<Policy> policies = new HashSet<>();
Mono<Long> commentSeq;
if (TRUE.equals(commentThread.getIsPrivate())) {
Collection<Policy> policyCollection = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD),
user
).values();
policies.addAll(policyCollection);
commentSeq = Mono.just(0L);
} else {
policies.addAll(policyGenerator.getAllChildPolicies(
application.getPolicies(),
Application.class,
CommentThread.class
));
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD),
user
).get(AclPermission.MANAGE_THREAD.getValue()));
commentSeq = sequenceService.getNext(CommentThread.class, application.getId());
}
commentThread.setPolicies(policies);
return commentSeq.map(sequenceNo -> {
commentThread.setSequenceId("#" + sequenceNo);
return sequenceNo;
}).then(threadRepository.save(commentThread));
}
private Mono<Comment> createBotComment(CommentThread commentThread, User user, CommentBotEvent commentBotEvent) {
final Comment comment = new Comment();
comment.setThreadId(commentThread.getId());
comment.setAuthorName(APPSMITH_BOT_NAME);
comment.setAuthorUsername(APPSMITH_BOT_USERNAME);
comment.setApplicationId(commentThread.getApplicationId());
comment.setOrgId(commentThread.getOrgId());
final Set<Policy> policies = policyGenerator.getAllChildPolicies(
commentThread.getPolicies(),
CommentThread.class,
Comment.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_COMMENT),
user
).get(AclPermission.MANAGE_COMMENT.getValue()));
comment.setPolicies(policies);
Comment.Block block = new Comment.Block();
Comment.Body body = new Comment.Body();
body.setBlocks(List.of(block));
comment.setBody(body);
block.setKey("key1");
Map<String, String> botCommentParams = new HashMap<>();
botCommentParams.put("AppsmithBotName", APPSMITH_BOT_NAME);
botCommentParams.put("AppsmithBotUserName", APPSMITH_BOT_USERNAME);
Map<String, Comment.Entity> entityMap = new HashMap<>();
try {
if (commentBotEvent == CommentBotEvent.COMMENTED) {
block.setText(TemplateUtils.parseTemplate(HOW_TO_TAG_BOT_COMMENT, botCommentParams));
block.setEntityRanges(List.of(new Comment.Range(92, APPSMITH_BOT_USERNAME.length(), 0)));
Comment.EntityData entityData = new Comment.EntityData();
entityData.setMention(new Comment.EntityData.Mention("appsmith", null));
Comment.Entity commentEntity = new Comment.Entity();
commentEntity.setType("mention");
commentEntity.setData(entityData);
entityMap.put("0", commentEntity);
} else {
block.setText(TemplateUtils.parseTemplate(HOW_TO_TAG_USER_COMMENT, botCommentParams));
}
} catch (IOException e) {
throw Exceptions.propagate(e);
}
block.setType("unstyled");
block.setDepth(0);
body.setEntityMap(entityMap);
return repository.save(comment);
}
@Override
public Mono<Long> getUnreadCount(String applicationId) {
return sessionUserService.getCurrentUser()

View File

@ -1,5 +1,7 @@
package com.appsmith.server.solutions;
import com.appsmith.server.configurations.EmailConfig;
import com.appsmith.server.constants.CommentConstants;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
@ -43,6 +45,7 @@ public class EmailEventHandler {
private final OrganizationRepository organizationRepository;
private final ApplicationRepository applicationRepository;
private final PolicyUtils policyUtils;
private final EmailConfig emailConfig;
public Mono<Boolean> publish(String authorUserName, String applicationId, Comment comment, String originHeader, Set<String> subscribers) {
if(CollectionUtils.isEmpty(subscribers)) { // no subscriber found, return without doing anything
@ -78,7 +81,7 @@ public class EmailEventHandler {
@Async
@EventListener
public void handle(CommentAddedEvent event) {
this.sendEmailForComment(
this.sendEmailForCommentAdded(
event.getAuthorUserName(),
event.getOrganization(),
event.getApplication(),
@ -92,7 +95,7 @@ public class EmailEventHandler {
@Async
@EventListener
public void handle(CommentThreadClosedEvent event) {
this.sendEmailForComment(
this.sendEmailForCommentThreadResolved(
event.getAuthorUserName(),
event.getOrganization(),
event.getApplication(),
@ -104,9 +107,9 @@ public class EmailEventHandler {
.subscribe();
}
private String getCommentThreadLink(Application application, String pageId, String threadId, UserRole userRole, String originHeader) {
private String getCommentThreadLink(Application application, String pageId, String threadId, String username, String originHeader) {
Boolean canManageApplication = policyUtils.isPermissionPresentForUser(
application.getPolicies(), MANAGE_APPLICATIONS.getValue(), userRole.getUsername()
application.getPolicies(), MANAGE_APPLICATIONS.getValue(), username
);
String urlPostfix = "/edit";
if (Boolean.FALSE.equals(canManageApplication)) { // user has no permission to manage application
@ -131,7 +134,7 @@ public class EmailEventHandler {
application,
commentThread.getPageId(),
commentThread.getId(),
receiverUserRole,
receiverUserRole.getUsername(),
originHeader)
);
templateParams.put("Resolved", true);
@ -157,7 +160,7 @@ public class EmailEventHandler {
application,
comment.getPageId(),
comment.getThreadId(),
receiverUserRole,
receiverUserRole.getUsername(),
originHeader)
);
@ -177,17 +180,48 @@ public class EmailEventHandler {
return emailSender.sendMail(receiverEmail, emailSubject, COMMENT_ADDED_EMAIL_TEMPLATE, templateParams);
}
private <E> Mono<Boolean> sendEmailForComment(String authorUserName, Organization organization, Application application, E commentDomain, String originHeader, Set<String> subscribers) {
private Mono<Boolean> geBotEmailSenderMono(Comment comment, String originHeader, Organization organization, Application application) {
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("App_User_Name", CommentConstants.APPSMITH_BOT_NAME);
templateParams.put("Commenter_Name", comment.getAuthorName());
templateParams.put("Application_Name", comment.getApplicationName());
templateParams.put("Organization_Name", organization.getName());
templateParams.put("Comment_Body", CommentUtils.getCommentBody(comment));
templateParams.put("commentUrl", getCommentThreadLink(
application,
comment.getPageId(),
comment.getThreadId(),
CommentConstants.APPSMITH_BOT_USERNAME,
originHeader)
);
templateParams.put("Mentioned", true);
String emailSubject = String.format("New comment for you from %s", comment.getAuthorName());
return emailSender.sendMail(
emailConfig.getSupportEmailAddress(), emailSubject, COMMENT_ADDED_EMAIL_TEMPLATE, templateParams
);
}
private Mono<Boolean> sendEmailForCommentAdded(String authorUserName, Organization organization, Application application, Comment comment, String originHeader, Set<String> subscribers) {
List<Mono<Boolean>> emailMonos = new ArrayList<>();
for (UserRole userRole : organization.getUserRoles()) {
if(!authorUserName.equals(userRole.getUsername()) && subscribers.contains(userRole.getUsername())) {
if(commentDomain instanceof Comment) {
Comment comment = (Comment)commentDomain;
emailMonos.add(getAddCommentEmailSenderMono(userRole, comment, originHeader, organization, application));
} else if(commentDomain instanceof CommentThread) {
CommentThread commentThread = (CommentThread) commentDomain;
emailMonos.add(getResolveThreadEmailSenderMono(userRole, commentThread, originHeader, organization, application));
}
emailMonos.add(getAddCommentEmailSenderMono(userRole, comment, originHeader, organization, application));
}
}
if(CommentUtils.isUserMentioned(comment, CommentConstants.APPSMITH_BOT_USERNAME)) {
emailMonos.add(geBotEmailSenderMono(comment, originHeader, organization, application));
}
return Flux.concat(emailMonos).then(Mono.just(Boolean.TRUE));
}
private Mono<Boolean> sendEmailForCommentThreadResolved(String authorUserName, Organization organization, Application application, CommentThread commentThread, String originHeader, Set<String> subscribers) {
List<Mono<Boolean>> emailMonos = new ArrayList<>();
for (UserRole userRole : organization.getUserRoles()) {
if(!authorUserName.equals(userRole.getUsername()) && subscribers.contains(userRole.getUsername())) {
emailMonos.add(getResolveThreadEmailSenderMono(userRole, commentThread, originHeader, organization, application));
}
}
return Flux.concat(emailMonos).then(Mono.just(Boolean.TRUE));

View File

@ -53,6 +53,7 @@ spring.redis.url=${APPSMITH_REDIS_URL}
# to send an email.
mail.enabled=${APPSMITH_MAIL_ENABLED:false}
mail.from=${APPSMITH_MAIL_FROM:appsmith@localhost}
mail.support=${APPSMITH_MAIL_SUPPORT:support@appsmith.com}
reply.to=${APPSMITH_REPLY_TO:appsmith@localhost}
spring.mail.host=${APPSMITH_MAIL_HOST:}
spring.mail.port=${APPSMITH_MAIL_PORT:}

View File

@ -0,0 +1,2 @@
Hello! 👋 I'm here to help you with your app!
You can get help from team appsmith by tagging {{AppsmithBotUserName}} in your comment. Feel free to say Hi!

View File

@ -0,0 +1,2 @@
Apps get shipped faster when team members are involved early on!
Invite a teammate to gather requirements, brainstorm, or get some feedback using the @ symbol followed by their email.

View File

@ -1,5 +1,6 @@
package com.appsmith.server.helpers;
import com.appsmith.server.constants.CommentConstants;
import com.appsmith.server.domains.Comment;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
@ -86,10 +87,10 @@ class CommentUtilsTest {
Comment comment = new Comment();
comment.setBody(body);
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "1"));
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "2"));
Assert.assertTrue(CommentUtils.isUserMentioned(comment, "3"));
Assert.assertFalse(CommentUtils.isUserMentioned(comment, "4"));
assertThat(CommentUtils.isUserMentioned(comment, "1")).isTrue();
assertThat(CommentUtils.isUserMentioned(comment, "2")).isTrue();
assertThat(CommentUtils.isUserMentioned(comment, "3")).isTrue();
assertThat(CommentUtils.isUserMentioned(comment, "4")).isFalse();
}
@Test
@ -120,4 +121,34 @@ class CommentUtilsTest {
assertThat(subscriberUsernames).contains("2");
assertThat(subscriberUsernames).contains("3");
}
@Test
void isAnyoneMentioned() {
// when comment has no body, expect false
Comment comment = new Comment();
assertThat(CommentUtils.isAnyoneMentioned(comment)).isFalse();
// when comment has body but no mention, expect false
Map<String, Comment.Entity> entityMap = createEntityMapForUsers(List.of());
Comment.Body body = new Comment.Body();
body.setEntityMap(entityMap);
comment.setBody(body);
assertThat(CommentUtils.isAnyoneMentioned(comment)).isFalse();
// when comment has body, but only bot is mentioed
Map<String, Comment.Entity> entityMap2 = createEntityMapForUsers(List.of(CommentConstants.APPSMITH_BOT_USERNAME));
Comment.Body body2 = new Comment.Body();
body2.setEntityMap(entityMap2);
comment.setBody(body2);
assertThat(CommentUtils.isAnyoneMentioned(comment)).isFalse();
// when comment has body with bot and other user mentioned
Map<String, Comment.Entity> entityMap3 = createEntityMapForUsers(
List.of(CommentConstants.APPSMITH_BOT_USERNAME, "2")
);
Comment.Body body3 = new Comment.Body();
body3.setEntityMap(entityMap3);
comment.setBody(body3);
assertThat(CommentUtils.isAnyoneMentioned(comment)).isTrue();
}
}

View File

@ -0,0 +1,113 @@
package com.appsmith.server.repositories;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.User;
import com.appsmith.server.helpers.PolicyUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomCommentThreadRepositoryImplTest {
@Autowired
CommentThreadRepository commentThreadRepository;
@Autowired
PolicyUtils policyUtils;
private CommentThread createThreadWithPolicies(String userEmail) {
CommentThread thread = new CommentThread();
User user = new User();
user.setEmail(userEmail);
Map<String, Policy> policyMap = policyUtils.generatePolicyFromPermission(Set.of(AclPermission.READ_THREAD), user);
thread.setPolicies(Set.copyOf(policyMap.values()));
return thread;
}
@Test
@WithUserDetails(value = "api_user")
public void addToSubscribers_WhenNoSubscriber_NewOnesAdded() {
CommentThread thread = createThreadWithPolicies("api_user");
Mono<CommentThread> commentThreadMono = commentThreadRepository.save(thread).flatMap(savedThread ->
commentThreadRepository.addToSubscribers(savedThread.getId(), Set.of("a", "b", "c"))
.thenReturn(savedThread)
).flatMap(commentThread -> commentThreadRepository.findById(commentThread.getId()));
StepVerifier.create(commentThreadMono).assertNext(commentThread -> {
assertThat(commentThread.getSubscribers().size()).isEqualTo(3);
assertThat(commentThread.getSubscribers()).contains("a", "b", "c");
}).verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void addToSubscribers_WhenSubscriberExists_NewOnesAdded() {
CommentThread thread = createThreadWithPolicies("api_user");
Mono<CommentThread> commentThreadMono = commentThreadRepository.save(thread).flatMap(savedThread ->
commentThreadRepository.addToSubscribers(savedThread.getId(), Set.of("a", "b", "c", "d"))
.thenReturn(savedThread)
).flatMap(commentThread -> commentThreadRepository.findById(commentThread.getId()));
StepVerifier.create(commentThreadMono).assertNext(commentThread -> {
assertThat(commentThread.getSubscribers().size()).isEqualTo(4);
assertThat(commentThread.getSubscribers()).contains("a", "b", "c", "d");
}).verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void findPrivateThread_WhenNoneExists_ReturnsEmpty() {
CommentThread thread = createThreadWithPolicies("api_user");
thread.setApplicationId("sample-application-id-1");
thread.setIsPrivate(true);
Mono<CommentThread> privateThreadMono = commentThreadRepository.save(thread)
.then(commentThreadRepository.findPrivateThread("sample-application-id-2"));
StepVerifier.create(privateThreadMono).verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void findPrivateThread_WhenOneExists_ReturnsOne() {
CommentThread thread1 = createThreadWithPolicies("api_user");
thread1.setApplicationId("sample-application-id-1");
thread1.setAuthorUsername("author1");
thread1.setIsPrivate(false);
CommentThread thread2 = createThreadWithPolicies("api_user2");
thread2.setApplicationId("sample-application-id-1");
thread2.setAuthorUsername("author2");
thread2.setIsPrivate(true);
CommentThread thread3 = createThreadWithPolicies("api_user");
thread3.setApplicationId("sample-application-id-1");
thread3.setAuthorUsername("author3");
thread3.setIsPrivate(true);
Mono<CommentThread> privateThreadMono = commentThreadRepository.saveAll(List.of(thread1, thread2, thread3))
.then(commentThreadRepository.findPrivateThread("sample-application-id-1"));
StepVerifier.create(privateThreadMono).assertNext(commentThread -> {
assertThat(commentThread.getAuthorUsername()).isEqualTo("author3");
}).verifyComplete();
}
}

View File

@ -4,7 +4,6 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionDTO;

View File

@ -1,6 +1,7 @@
package com.appsmith.server.solutions;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.configurations.EmailConfig;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Comment;
import com.appsmith.server.domains.CommentThread;
@ -44,6 +45,8 @@ public class EmailEventHandlerTest {
private OrganizationRepository organizationRepository;
@MockBean
private ApplicationRepository applicationRepository;
@MockBean
private EmailConfig emailConfig;
@MockBean
private PolicyUtils policyUtils;
@ -61,7 +64,7 @@ public class EmailEventHandlerTest {
@Before
public void setUp() {
emailEventHandler = new EmailEventHandler(
applicationEventPublisher, emailSender, organizationRepository, applicationRepository, policyUtils
applicationEventPublisher, emailSender, organizationRepository, applicationRepository, policyUtils, emailConfig
);
application = new Application();
application.setName("Test application for comment");