/* * 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/eventqueue" "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/activityqueue" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/export" "github.com/apache/answer/internal/service/noticequeue" "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/internal/service/vector_sync" "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 noticequeue.Service externalNotificationQueueService noticequeue.ExternalService activityQueueService activityqueue.Service eventQueueService eventqueue.Service reviewService *review.ReviewService vectorSyncService vector_sync.Service } // 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 noticequeue.Service, externalNotificationQueueService noticequeue.ExternalService, activityQueueService activityqueue.Service, eventQueueService eventqueue.Service, reviewService *review.ReviewService, vectorSyncService vector_sync.Service, ) *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, vectorSyncService: vectorSyncService, } } // 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 { if err := cs.addCommentNotification(ctx, req, resp, comment, objInfo); err != nil { return nil, 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) if comment.Status == entity.CommentStatusAvailable { switch objInfo.ObjectType { case constant.QuestionObjectType: cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: objInfo.QuestionID}) case constant.AnswerObjectType: cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: objInfo.AnswerID}) cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: objInfo.QuestionID}) } } return resp, nil } func (cs *CommentService) addCommentNotification( ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp, comment *entity.Comment, objInfo *schema.SimpleObjectInfo) 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 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 } 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 } 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 } // RemoveComment delete comment func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) { commentInfo, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) if err != nil { return err } if !exist { return nil } 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)) if commentInfo.ObjectID == commentInfo.QuestionID { cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: commentInfo.QuestionID}) } else { cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: commentInfo.ObjectID}) cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: commentInfo.QuestionID}) } 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)) if old.ObjectID == old.QuestionID { cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: old.QuestionID}) } else { cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: old.ObjectID}) cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: old.QuestionID}) } 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) } objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID) if err != nil { return nil, err } if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil { return nil, err } 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) { objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return nil, err } if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil { return nil, err } 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 }