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:
parent
56caba8f55
commit
8fcbb6e4c1
|
|
@ -22,4 +22,7 @@ public class EmailConfig {
|
|||
@Value("${emails.welcome.enabled:true}")
|
||||
private boolean isWelcomeEmailEnabled;
|
||||
|
||||
@Value("${mail.support}")
|
||||
private String supportEmailAddress;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.appsmith.server.constants;
|
||||
|
||||
public class Appsmith {
|
||||
|
||||
public final static String APPSMITH_REGISTERED = "appsmith_registered";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.appsmith.server.constants;
|
||||
|
||||
public enum CommentBotEvent {
|
||||
COMMENTED, RESOLVED
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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:}
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
@ -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.
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue