blob: 3a5b3e6c9bef7d561bafe746d96316472529e7ba [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package comment
import (
"context"
"github.com/apache/answer/internal/service/event_queue"
"github.com/apache/answer/internal/service/review"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/activity_queue"
"github.com/apache/answer/internal/service/comment_common"
"github.com/apache/answer/internal/service/export"
"github.com/apache/answer/internal/service/notice_queue"
"github.com/apache/answer/internal/service/object_info"
"github.com/apache/answer/internal/service/permission"
usercommon "github.com/apache/answer/internal/service/user_common"
"github.com/apache/answer/pkg/htmltext"
"github.com/apache/answer/pkg/token"
"github.com/apache/answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// CommentRepo comment repository
type CommentRepo interface {
AddComment(ctx context.Context, comment *entity.Comment) (err error)
RemoveComment(ctx context.Context, commentID string) (err error)
UpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error)
UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
GetCommentPage(ctx context.Context, commentQuery *CommentQuery) (
comments []*entity.Comment, total int64, err error)
}
type CommentQuery struct {
pager.PageCond
// object id
ObjectID string
// query condition
QueryCond string
// user id
UserID string
}
func (c *CommentQuery) GetOrderBy() string {
if c.QueryCond == "vote" {
return "vote_count DESC,created_at ASC"
}
if c.QueryCond == "created_at" {
return "created_at DESC"
}
return "created_at ASC"
}
// CommentService user service
type CommentService struct {
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
eventQueueService event_queue.EventQueueService
reviewService *review.ReviewService
}
// NewCommentService new comment service
func NewCommentService(
commentRepo CommentRepo,
commentCommonRepo comment_common.CommentCommonRepo,
userCommon *usercommon.UserCommon,
objectInfoService *object_info.ObjService,
voteCommon activity_common.VoteRepo,
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
eventQueueService event_queue.EventQueueService,
reviewService *review.ReviewService,
) *CommentService {
return &CommentService{
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
eventQueueService: eventQueueService,
reviewService: reviewService,
}
}
// AddComment add comment
func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddCommentReq) (
resp *schema.GetCommentResp, err error) {
comment := &entity.Comment{}
_ = copier.Copy(comment, req)
comment.Status = entity.CommentStatusAvailable
objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)
if err != nil {
return nil, err
}
if objInfo.IsDeleted() {
return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
}
objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)
objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
if objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType {
comment.QuestionID = objInfo.QuestionID
}
if len(req.ReplyCommentID) > 0 {
replyComment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ReplyCommentID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.CommentNotFound)
}
comment.SetReplyUserID(replyComment.UserID)
comment.SetReplyCommentID(replyComment.ID)
} else {
comment.SetReplyUserID("")
comment.SetReplyCommentID("")
}
err = cs.commentRepo.AddComment(ctx, comment)
if err != nil {
return nil, err
}
comment.Status = cs.reviewService.AddCommentReview(ctx, comment, req.IP, req.UserAgent)
if err := cs.commentRepo.UpdateCommentStatus(ctx, comment.ID, comment.Status); err != nil {
return nil, err
}
resp = &schema.GetCommentResp{}
resp.SetFromComment(comment)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
time.Now(), req.CanEdit, req.CanDelete)
if comment.Status == entity.CommentStatusAvailable {
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
if err != nil {
return commentResp, err
}
}
// get user info
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
if err != nil {
return nil, err
}
if exist {
resp.Username = userInfo.Username
resp.UserDisplayName = userInfo.DisplayName
resp.UserAvatar = userInfo.Avatar
resp.UserStatus = userInfo.Status
}
activityMsg := &schema.ActivityMsg{
UserID: comment.UserID,
ObjectID: comment.ID,
OriginalObjectID: req.ObjectID,
ActivityTypeKey: constant.ActQuestionCommented,
}
var event *schema.EventMsg
switch objInfo.ObjectType {
case constant.QuestionObjectType:
activityMsg.ActivityTypeKey = constant.ActQuestionCommented
event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).
CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID)
case constant.AnswerObjectType:
activityMsg.ActivityTypeKey = constant.ActAnswerCommented
event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).
CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID)
}
cs.activityQueueService.Send(ctx, activityMsg)
cs.eventQueueService.Send(ctx, event)
return resp, nil
}
func (cs *CommentService) addCommentNotification(
ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,
comment *entity.Comment, objInfo *schema.SimpleObjectInfo) (*schema.GetCommentResp, error) {
// The priority of the notification
// 1. reply to user
// 2. comment mention to user
// 3. answer or question was commented
alreadyNotifiedUserID := make(map[string]bool)
// get reply user info
if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID,
objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
alreadyNotifiedUserID[replyUser.ID] = true
return nil, nil
}
if len(req.MentionUsernameList) > 0 {
alreadyNotifiedUserIDs := cs.notificationMention(
ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID)
for _, userID := range alreadyNotifiedUserIDs {
alreadyNotifiedUserID[userID] = true
}
return nil, nil
}
if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
}
return nil, nil
}
// RemoveComment delete comment
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
err = cs.commentRepo.RemoveComment(ctx, req.CommentID)
if err != nil {
return err
}
cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID).
TID(req.CommentID).CID(req.CommentID, req.UserID))
return nil
}
// UpdateComment update comment
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (
resp *schema.UpdateCommentResp, err error) {
old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.CommentNotFound)
}
// user can't edit the comment that was posted by others except admin
if !req.IsAdmin && req.UserID != old.UserID {
return nil, errors.BadRequest(reason.CommentNotFound)
}
// user can edit the comment that was posted by himself before deadline.
// admin can edit it at any time
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
return nil, errors.BadRequest(reason.CommentCannotEditAfterDeadline)
}
if err = cs.commentRepo.UpdateCommentContent(ctx, old.ID, req.OriginalText, req.ParsedText); err != nil {
return nil, err
}
resp = &schema.UpdateCommentResp{
CommentID: old.ID,
OriginalText: req.OriginalText,
ParsedText: req.ParsedText,
}
cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID).
CID(old.ID, old.UserID))
return resp, nil
}
// GetComment get comment one
func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetCommentReq) (resp *schema.GetCommentResp, err error) {
comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ID)
if err != nil {
return
}
if !exist {
return nil, errors.BadRequest(reason.CommentNotFound)
}
resp = &schema.GetCommentResp{
CommentID: comment.ID,
CreatedAt: comment.CreatedAt.Unix(),
UserID: comment.UserID,
ReplyUserID: comment.GetReplyUserID(),
ReplyCommentID: comment.GetReplyCommentID(),
ObjectID: comment.ObjectID,
VoteCount: comment.VoteCount,
OriginalText: comment.OriginalText,
ParsedText: comment.ParsedText,
}
// get comment user info
if len(resp.UserID) > 0 {
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
if err != nil {
return nil, err
}
if exist {
resp.Username = commentUser.Username
resp.UserDisplayName = commentUser.DisplayName
resp.UserAvatar = commentUser.Avatar
resp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(resp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
}
// check if current user vote this comment
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
comment.CreatedAt, req.CanEdit, req.CanDelete)
return resp, nil
}
// GetCommentWithPage get comment list page
func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) (
pageModel *pager.PageModel, err error) {
dto := &CommentQuery{
PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize},
ObjectID: req.ObjectID,
QueryCond: req.QueryCond,
}
commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)
if err != nil {
return nil, err
}
resp := make([]*schema.GetCommentResp, 0)
for _, comment := range commentList {
commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
if err != nil {
return nil, err
}
resp = append(resp, commentResp)
}
// if user request the specific comment, add it if not exist.
if len(req.CommentID) > 0 {
commentExist := false
for _, t := range resp {
if t.CommentID == req.CommentID {
commentExist = true
break
}
}
if !commentExist {
comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
if err != nil {
return nil, err
}
if exist && comment.ObjectID == req.ObjectID {
commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
if err != nil {
return nil, err
}
resp = append(resp, commentResp)
}
}
}
return pager.NewPageModel(total, resp), nil
}
func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq,
comment *entity.Comment) (commentResp *schema.GetCommentResp, err error) {
commentResp = &schema.GetCommentResp{
CommentID: comment.ID,
CreatedAt: comment.CreatedAt.Unix(),
UserID: comment.UserID,
ReplyUserID: comment.GetReplyUserID(),
ReplyCommentID: comment.GetReplyCommentID(),
ObjectID: comment.ObjectID,
VoteCount: comment.VoteCount,
OriginalText: comment.OriginalText,
ParsedText: comment.ParsedText,
}
// get comment user info
if len(commentResp.UserID) > 0 {
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)
if err != nil {
return nil, err
}
if exist {
commentResp.Username = commentUser.Username
commentResp.UserDisplayName = commentUser.DisplayName
commentResp.UserAvatar = commentUser.Avatar
commentResp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(commentResp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
commentResp.ReplyUsername = replyUser.Username
commentResp.ReplyUserDisplayName = replyUser.DisplayName
commentResp.ReplyUserStatus = replyUser.Status
}
}
// check if current user vote this comment
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
commentResp.MemberActions = permission.GetCommentPermission(ctx,
req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)
return commentResp, nil
}
func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) {
status := cs.voteCommon.GetVoteStatus(ctx, commentID, userID)
return len(status) > 0
}
// GetCommentPersonalWithPage get personal comment list page
func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *schema.GetCommentPersonalWithPageReq) (
pageModel *pager.PageModel, err error) {
if len(req.Username) > 0 {
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
req.UserID = userInfo.ID
}
if len(req.UserID) == 0 {
return nil, errors.BadRequest(reason.UserNotFound)
}
dto := &CommentQuery{
PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize},
UserID: req.UserID,
QueryCond: "created_at",
}
commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)
if err != nil {
return nil, err
}
resp := make([]*schema.GetCommentPersonalWithPageResp, 0)
for _, comment := range commentList {
commentResp := &schema.GetCommentPersonalWithPageResp{
CommentID: comment.ID,
CreatedAt: comment.CreatedAt.Unix(),
ObjectID: comment.ObjectID,
Content: comment.ParsedText, // todo trim
}
if len(comment.ObjectID) > 0 {
objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
if err != nil {
log.Error(err)
} else {
commentResp.ObjectType = objInfo.ObjectType
commentResp.Title = objInfo.Title
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
commentResp.QuestionID = objInfo.QuestionID
commentResp.AnswerID = objInfo.AnswerID
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
commentResp.Title = "Deleted question"
}
}
}
resp = append(resp, commentResp)
}
return pager.NewPageModel(total, resp), nil
}
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID,
questionID, questionTitle, commentID, commentUserID, commentSummary string) {
if questionUserID == commentUserID {
return
}
// send internal notification
msg := &schema.NotificationMsg{
ReceiverUserID: questionUserID,
TriggerUserID: commentUserID,
Type: schema.NotificationTypeInbox,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentQuestion
cs.notificationQueueService.Send(ctx, msg)
// send external notification
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {
log.Error(err)
return
}
if !exist {
log.Warnf("user %s not found", questionUserID)
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
CommentID: commentID,
CommentSummary: commentSummary,
UnsubscribeCode: token.GenerateToken(),
}
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationAnswerComment(ctx context.Context,
questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {
if answerUserID == commentUserID {
return
}
// Send internal notification.
msg := &schema.NotificationMsg{
ReceiverUserID: answerUserID,
TriggerUserID: commentUserID,
Type: schema.NotificationTypeInbox,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentAnswer
cs.notificationQueueService.Send(ctx, msg)
// Send external notification.
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
if err != nil {
log.Error(err)
return
}
if !exist {
log.Warnf("user %s not found", answerUserID)
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
AnswerID: answerID,
CommentID: commentID,
CommentSummary: commentSummary,
UnsubscribeCode: token.GenerateToken(),
}
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID,
questionID, questionTitle, commentSummary string) {
msg := &schema.NotificationMsg{
ReceiverUserID: replyUserID,
TriggerUserID: commentUserID,
Type: schema.NotificationTypeInbox,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationReplyToYou
cs.notificationQueueService.Send(ctx, msg)
// Send external notification.
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID)
if err != nil {
log.Error(err)
return
}
if !exist {
log.Warnf("user %s not found", replyUserID)
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
CommentID: commentID,
CommentSummary: commentSummary,
UnsubscribeCode: token.GenerateToken(),
}
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationMention(
ctx context.Context, mentionUsernameList []string, commentID, commentUserID string,
alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {
for _, username := range mentionUsernameList {
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
if err != nil {
log.Error(err)
continue
}
if exist && !alreadyNotifiedUserID[userInfo.ID] {
msg := &schema.NotificationMsg{
ReceiverUserID: userInfo.ID,
TriggerUserID: commentUserID,
Type: schema.NotificationTypeInbox,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationMentionYou
cs.notificationQueueService.Send(ctx, msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
}
}
return alreadyNotifiedUserIDs
}