From e12e0094d2cf4860435c1d0e0e0d69ba9a5b89de Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 24 May 2023 10:35:38 +0800 Subject: [PATCH] feat(notification): add email notification for invited to answer --- cmd/wire_gen.go | 2 +- i18n/en_US.yaml | 5 ++ i18n/zh_CN.yaml | 11 +++- internal/base/constant/email_tpl_key.go | 3 + internal/schema/email_template.go | 15 +++++ internal/service/export/email_service.go | 23 ++++++- internal/service/question_service.go | 81 ++++++++++++++++++++++-- 7 files changed, 129 insertions(+), 11 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index e88de43f..f50f34a2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -170,7 +170,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData) + questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, dataData, emailService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService) questionController := controller.NewQuestionController(questionService, answerService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 901ec26a..dbfbb39a 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -401,6 +401,11 @@ backend: other: "[{{.SiteName}}] {{.DisplayName}} answered your question" body: other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index ca2edf49..6329718e 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -378,6 +378,8 @@ backend: other: 踩了答案 up_voted_comment: other: 赞了评论 + invited_you_to_answer: + other: 邀请你回答问题 email_tpl: change_email: title: @@ -388,12 +390,17 @@ backend: title: other: "[{{.SiteName}}] {{.DisplayName}} 回答了您的问题" body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您是该讨论的作者。取消订阅" + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅。取消订阅" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅. 取消订阅" new_comment: title: other: "[{{.SiteName}}] {{.DisplayName}} 评论了您的帖子" body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您是该讨论的作者。取消订阅" + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n您会收到此邮件是因为您开启了订阅。取消订阅" pass_reset: title: other: "[{{.SiteName }}] 重置密码" diff --git a/internal/base/constant/email_tpl_key.go b/internal/base/constant/email_tpl_key.go index 178f2d30..cb191165 100644 --- a/internal/base/constant/email_tpl_key.go +++ b/internal/base/constant/email_tpl_key.go @@ -18,4 +18,7 @@ const ( EmailTplKeyTestTitle = "email_tpl.test.title" EmailTplKeyTestBody = "email_tpl.test.body" + + EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title" + EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body" ) diff --git a/internal/schema/email_template.go b/internal/schema/email_template.go index 94918331..01cc1667 100644 --- a/internal/schema/email_template.go +++ b/internal/schema/email_template.go @@ -47,6 +47,21 @@ type NewAnswerTemplateData struct { UnsubscribeUrl string } +type NewInviteAnswerTemplateRawData struct { + InviterDisplayName string + QuestionTitle string + QuestionID string + UnsubscribeCode string +} + +type NewInviteAnswerTemplateData struct { + SiteName string + DisplayName string + QuestionTitle string + InviteUrl string + UnsubscribeUrl string +} + type NewCommentTemplateRawData struct { CommentUserDisplayName string QuestionTitle string diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 63b26598..4fd2fa9f 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -242,7 +242,6 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn AnswerSummary: raw.AnswerSummary, UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } - templateData.SiteName = siteInfo.Name lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) @@ -250,6 +249,27 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn return title, body, nil } +// NewInviteAnswerTemplate new invite answer template +func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.GetSiteGeneral(ctx) + if err != nil { + return + } + templateData := &schema.NewInviteAnswerTemplateData{ + SiteName: siteInfo.Name, + DisplayName: raw.InviterDisplayName, + QuestionTitle: raw.QuestionTitle, + InviteUrl: fmt.Sprintf("%s/questions/%s", siteInfo.SiteUrl, raw.QuestionID), + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), + } + + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) + return title, body, nil +} + // NewCommentTemplate new comment template func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( title, body string, err error) { @@ -271,7 +291,6 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl, raw.QuestionID, raw.CommentID) } - templateData.SiteName = siteInfo.Name lang := handler.GetLangByCtx(ctx) title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index a705265f..a2ae66ba 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -19,6 +19,7 @@ import ( "github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity_queue" collectioncommon "github.com/answerdev/answer/internal/service/collection_common" + "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/meta" "github.com/answerdev/answer/internal/service/notice_queue" "github.com/answerdev/answer/internal/service/permission" @@ -26,10 +27,12 @@ import ( "github.com/answerdev/answer/internal/service/revision_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/answerdev/answer/pkg/encryption" "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) @@ -42,11 +45,13 @@ type QuestionService struct { tagCommon *tagcommon.TagCommonService questioncommon *questioncommon.QuestionCommon userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo revisionService *revision_common.RevisionService metaService *meta.MetaService collectionCommon *collectioncommon.CollectionCommon answerActivityService *activity.AnswerActivityService data *data.Data + emailService *export.EmailService } func NewQuestionService( @@ -54,23 +59,26 @@ func NewQuestionService( tagCommon *tagcommon.TagCommonService, questioncommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, + userRepo usercommon.UserRepo, revisionService *revision_common.RevisionService, metaService *meta.MetaService, collectionCommon *collectioncommon.CollectionCommon, answerActivityService *activity.AnswerActivityService, data *data.Data, - + emailService *export.EmailService, ) *QuestionService { return &QuestionService{ questionRepo: questionRepo, tagCommon: tagCommon, questioncommon: questioncommon, userCommon: userCommon, + userRepo: userRepo, revisionService: revisionService, metaService: metaService, collectionCommon: collectionCommon, answerActivityService: answerActivityService, data: data, + emailService: emailService, } } @@ -536,20 +544,28 @@ func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *sch } func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) { + originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + if !exist { + return errors.NotFound(reason.ObjectNotFound) + } + //verify invite user inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser) if err != nil { log.Error("BatchGetUserBasicInfoByUserNames error", err.Error()) } - inviteUser := make([]string, 0) + inviteUserIDs := make([]string, 0) for _, item := range req.InviteUser { _, ok := inviteUserInfoList[item] if ok { - inviteUser = append(inviteUser, inviteUserInfoList[item].ID) + inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID) } } inviteUserStr := "" - inviteUserByte, err := json.Marshal(inviteUser) + inviteUserByte, err := json.Marshal(inviteUserIDs) if err != nil { log.Error("json.Marshal error", err.Error()) inviteUserStr = "[]" @@ -564,12 +580,31 @@ func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *sc if saveerr != nil { return saveerr } - qs.notificationInviteUser(ctx, inviteUser, req.ID, req.UserID) + go qs.notificationInviteUser(ctx, inviteUserIDs, originQuestion.ID, originQuestion.Title, req.UserID) return nil } func (qs *QuestionService) notificationInviteUser( - ctx context.Context, invitedUserIDs []string, questionID, questionUserID string) { + ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) { + inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs) + if err != nil { + log.Error(err) + return + } + invitee := make(map[string]*entity.User, len(users)) + for _, user := range users { + invitee[user.ID] = user + } for _, userID := range invitedUserIDs { msg := &schema.NotificationMsg{ ReceiverUserID: userID, @@ -580,6 +615,40 @@ func (qs *QuestionService) notificationInviteUser( msg.ObjectType = constant.QuestionObjectType msg.NotificationAction = constant.NotificationInvitedYouToAnswer notice_queue.AddNotification(msg) + + userInfo, ok := invitee[userID] + if !ok { + log.Warnf("user %s not found", userID) + return + } + if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 { + return + } + + rawData := &schema.NewInviteAnswerTemplateRawData{ + InviterDisplayName: inviter.DisplayName, + QuestionTitle: questionTitle, + QuestionID: questionID, + UnsubscribeCode: encryption.MD5(userInfo.Pass), + } + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + Email: userInfo.EMail, + UserID: userInfo.ID, + } + + // If receiver has set language, use it to send email. + if len(userInfo.Language) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language)) + } + title, body, err := qs.emailService.NewInviteAnswerTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + go qs.emailService.SendAndSaveCodeWithTime( + ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour) } }