mirror of https://gitee.com/answerdev/answer.git
feat(notification): Email notice when a user answers or comment
This commit is contained in:
parent
27e84f9ea8
commit
81f914d72f
|
@ -135,7 +135,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService)
|
||||
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
|
||||
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo)
|
||||
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
|
||||
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
|
||||
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configRepo)
|
||||
|
@ -166,7 +166,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
|
||||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
|
||||
questionController := controller.NewQuestionController(questionService, rankService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
|
||||
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
|
||||
|
|
|
@ -152,7 +152,7 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
id := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
titleIsAnswerID := false
|
||||
|
@ -182,6 +182,9 @@ func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *sc
|
|||
return
|
||||
}
|
||||
url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
|
||||
if titleIsAnswerID {
|
||||
url = fmt.Sprintf("%s/%s", url, title)
|
||||
}
|
||||
return true, url
|
||||
}
|
||||
}
|
||||
|
@ -217,9 +220,9 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
jump, jumpurl := tc.QuestionInfo301Jump(ctx, siteInfo, correctTitle)
|
||||
jump, jumpurl := tc.QuestionInfoeRdirect(ctx, siteInfo, correctTitle)
|
||||
if jump {
|
||||
ctx.Redirect(http.StatusMovedPermanently, jumpurl)
|
||||
ctx.Redirect(http.StatusFound, jumpurl)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -535,3 +535,28 @@ func (uc *UserController) UserRanking(ctx *gin.Context) {
|
|||
resp, err := uc.userService.UserRanking(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotification unsubscribe email notification
|
||||
// @Summary unsubscribe email notification
|
||||
// @Description unsubscribe email notification
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/user/email/notification [put]
|
||||
func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
|
||||
req := &schema.UserUnsubscribeEmailNotificationReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
|
||||
if len(req.Content) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired),
|
||||
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired})
|
||||
return
|
||||
}
|
||||
|
||||
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ func NewEmailRepo(data *data.Data) export.EmailRepo {
|
|||
}
|
||||
|
||||
// SetCode The email code is used to verify that the link in the message is out of date
|
||||
func (e *emailRepo) SetCode(ctx context.Context, code, content string) error {
|
||||
err := e.data.Cache.SetString(ctx, code, content, 10*time.Minute)
|
||||
func (e *emailRepo) SetCode(ctx context.Context, code, content string, duration time.Duration) error {
|
||||
err := e.data.Cache.SetString(ctx, code, content, duration)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
|
|||
r.POST("/user/password/reset", a.userController.RetrievePassWord)
|
||||
r.POST("/user/password/replacement", a.userController.UseRePassWord)
|
||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||
r.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package schema
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
AccountActivationSourceType SourceType = "account-activation"
|
||||
PasswordResetSourceType SourceType = "password-reset"
|
||||
ConfirmNewEmailSourceType SourceType = "password-reset"
|
||||
UnsubscribeSourceType SourceType = "unsubscribe"
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
|
||||
type EmailCodeContent struct {
|
||||
SourceType SourceType `json:"source_type"`
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) ToJSONString() string {
|
||||
codeBytes, _ := json.Marshal(r)
|
||||
return string(codeBytes)
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) FromJSONString(data string) error {
|
||||
return json.Unmarshal([]byte(data), &r)
|
||||
}
|
||||
|
||||
type NewAnswerTemplateRawData struct {
|
||||
AnswerUserDisplayName string
|
||||
QuestionTitle string
|
||||
QuestionID string
|
||||
AnswerID string
|
||||
AnswerSummary string
|
||||
UnsubscribeCode string
|
||||
}
|
||||
|
||||
type NewAnswerTemplateData struct {
|
||||
SiteName string
|
||||
DisplayName string
|
||||
QuestionTitle string
|
||||
AnswerUrl string
|
||||
AnswerSummary string
|
||||
UnsubscribeUrl string
|
||||
}
|
||||
|
||||
type NewCommentTemplateRawData struct {
|
||||
CommentUserDisplayName string
|
||||
QuestionTitle string
|
||||
QuestionID string
|
||||
AnswerID string
|
||||
CommentID string
|
||||
CommentSummary string
|
||||
UnsubscribeCode string
|
||||
}
|
||||
|
||||
type NewCommentTemplateData struct {
|
||||
SiteName string
|
||||
DisplayName string
|
||||
QuestionTitle string
|
||||
CommentUrl string
|
||||
CommentSummary string
|
||||
UnsubscribeUrl string
|
||||
}
|
|
@ -349,8 +349,8 @@ func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField,
|
|||
}
|
||||
|
||||
type UserNoticeSetRequest struct {
|
||||
UserID string `json:"-" ` // user_id
|
||||
NoticeSwitch bool `json:"notice_switch" `
|
||||
NoticeSwitch bool `json:"notice_switch"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type UserNoticeSetResp struct {
|
||||
|
@ -396,20 +396,6 @@ type UserChangeEmailSendCodeReq struct {
|
|||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
type EmailCodeContent struct {
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) ToJSONString() string {
|
||||
codeBytes, _ := json.Marshal(r)
|
||||
return string(codeBytes)
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) FromJSONString(data string) error {
|
||||
return json.Unmarshal([]byte(data), &r)
|
||||
}
|
||||
|
||||
type UserChangeEmailVerifyReq struct {
|
||||
Code string `validate:"required,gt=0,lte=500" json:"code"`
|
||||
Content string `json:"-"`
|
||||
|
@ -440,3 +426,9 @@ type UserRankingSimpleInfo struct {
|
|||
// avatar
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotificationReq user unsubscribe email notification request
|
||||
type UserUnsubscribeEmailNotificationReq struct {
|
||||
Code string `validate:"required,gt=0,lte=500" json:"code"`
|
||||
Content string `json:"-"`
|
||||
}
|
||||
|
|
|
@ -15,11 +15,13 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
answercommon "github.com/answerdev/answer/internal/service/answer_common"
|
||||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
@ -36,6 +38,7 @@ type AnswerService struct {
|
|||
revisionService *revision_common.RevisionService
|
||||
AnswerCommon *answercommon.AnswerCommon
|
||||
voteRepo activity_common.VoteRepo
|
||||
emailService *export.EmailService
|
||||
}
|
||||
|
||||
func NewAnswerService(
|
||||
|
@ -49,6 +52,7 @@ func NewAnswerService(
|
|||
answerAcceptActivityRepo *activity.AnswerActivityService,
|
||||
answerCommon *answercommon.AnswerCommon,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
) *AnswerService {
|
||||
return &AnswerService{
|
||||
answerRepo: answerRepo,
|
||||
|
@ -61,6 +65,7 @@ func NewAnswerService(
|
|||
answerActivityService: answerAcceptActivityRepo,
|
||||
AnswerCommon: answerCommon,
|
||||
voteRepo: voteRepo,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,7 +181,8 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
|
|||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID)
|
||||
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,
|
||||
insertData.OriginalText)
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: insertData.UserID,
|
||||
|
@ -542,7 +548,12 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
|
|||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, questionUserID, answerID, answerUserID string) {
|
||||
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
||||
questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {
|
||||
// If the question is answered by me, there is no notification for myself.
|
||||
if questionUserID == answerUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: answerUserID,
|
||||
ReceiverUserID: questionUserID,
|
||||
|
@ -552,4 +563,43 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, ques
|
|||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AnswerTheQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewAnswerTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
AnswerSummary: answerSummary,
|
||||
UnsubscribeCode: encryption.MD5(userInfo.Pass),
|
||||
}
|
||||
answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
|
||||
if answerUser != nil {
|
||||
rawData.AnswerUserDisplayName = answerUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: userInfo.EMail,
|
||||
UserID: userInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := as.emailService.NewAnswerTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go as.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package comment
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
|
@ -11,10 +12,12 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/encryption"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -30,15 +33,6 @@ type CommentRepo interface {
|
|||
comments []*entity.Comment, total int64, err error)
|
||||
}
|
||||
|
||||
// CommentService user service
|
||||
type CommentService struct {
|
||||
commentRepo CommentRepo
|
||||
commentCommonRepo comment_common.CommentCommonRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
voteCommon activity_common.VoteRepo
|
||||
objectInfoService *object_info.ObjService
|
||||
}
|
||||
|
||||
type CommentQuery struct {
|
||||
pager.PageCond
|
||||
// object id
|
||||
|
@ -59,19 +53,35 @@ func (c *CommentQuery) GetOrderBy() string {
|
|||
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
|
||||
}
|
||||
|
||||
// NewCommentService new comment service
|
||||
func NewCommentService(
|
||||
commentRepo CommentRepo,
|
||||
commentCommonRepo comment_common.CommentCommonRepo,
|
||||
userCommon *usercommon.UserCommon,
|
||||
objectInfoService *object_info.ObjService,
|
||||
voteCommon activity_common.VoteRepo) *CommentService {
|
||||
voteCommon activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
userRepo usercommon.UserRepo,
|
||||
) *CommentService {
|
||||
return &CommentService{
|
||||
commentRepo: commentRepo,
|
||||
commentCommonRepo: commentCommonRepo,
|
||||
userCommon: userCommon,
|
||||
voteCommon: voteCommon,
|
||||
objectInfoService: objectInfoService,
|
||||
emailService: emailService,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,9 +122,11 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType {
|
||||
cs.notificationAnswerComment(ctx, objInfo.ObjectCreatorUserID, comment.ID, req.UserID)
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
|
||||
|
@ -401,7 +413,11 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
|
|||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, commentID, commentUserID string) {
|
||||
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID,
|
||||
questionID, questionTitle, commentID, commentUserID, commentSummary string) {
|
||||
if questionUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: questionUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -411,9 +427,52 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
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
|
||||
}
|
||||
if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: receiverUserInfo.EMail,
|
||||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go cs.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, receiverUserInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerUserID, commentID, commentUserID string) {
|
||||
func (cs *CommentService) notificationAnswerComment(ctx context.Context,
|
||||
questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {
|
||||
if answerUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: answerUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -423,6 +482,46 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerU
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
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
|
||||
}
|
||||
if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
codeContent := &schema.EmailCodeContent{
|
||||
SourceType: schema.UnsubscribeSourceType,
|
||||
Email: receiverUserInfo.EMail,
|
||||
UserID: receiverUserInfo.ID,
|
||||
}
|
||||
|
||||
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go cs.emailService.SendAndSaveCodeWithTime(
|
||||
ctx, receiverUserInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -27,7 +28,7 @@ type EmailService struct {
|
|||
|
||||
// EmailRepo email repository
|
||||
type EmailRepo interface {
|
||||
SetCode(ctx context.Context, code, content string) error
|
||||
SetCode(ctx context.Context, code, content string, duration time.Duration) error
|
||||
VerifyCode(ctx context.Context, code string) (content string, err error)
|
||||
}
|
||||
|
||||
|
@ -51,14 +52,18 @@ type EmailConfig struct {
|
|||
SMTPPassword string `json:"smtp_password"`
|
||||
SMTPAuthentication bool `json:"smtp_authentication"`
|
||||
|
||||
RegisterTitle string `json:"register_title"`
|
||||
RegisterBody string `json:"register_body"`
|
||||
PassResetTitle string `json:"pass_reset_title"`
|
||||
PassResetBody string `json:"pass_reset_body"`
|
||||
ChangeTitle string `json:"change_title"`
|
||||
ChangeBody string `json:"change_body"`
|
||||
TestTitle string `json:"test_title"`
|
||||
TestBody string `json:"test_body"`
|
||||
RegisterTitle string `json:"register_title"`
|
||||
RegisterBody string `json:"register_body"`
|
||||
PassResetTitle string `json:"pass_reset_title"`
|
||||
PassResetBody string `json:"pass_reset_body"`
|
||||
ChangeTitle string `json:"change_title"`
|
||||
ChangeBody string `json:"change_body"`
|
||||
TestTitle string `json:"test_title"`
|
||||
TestBody string `json:"test_body"`
|
||||
NewAnswerTitle string `json:"new_answer_title"`
|
||||
NewAnswerBody string `json:"new_answer_body"`
|
||||
NewCommentTitle string `json:"new_comment_title"`
|
||||
NewCommentBody string `json:"new_comment_body"`
|
||||
}
|
||||
|
||||
func (e *EmailConfig) IsSSL() bool {
|
||||
|
@ -84,8 +89,27 @@ type TestTemplateData struct {
|
|||
SiteName string
|
||||
}
|
||||
|
||||
// SendAndSaveCode send email and save code
|
||||
func (es *EmailService) SendAndSaveCode(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
|
||||
es.Send(ctx, toEmailAddr, subject, body)
|
||||
err := es.emailRepo.SetCode(ctx, code, codeContent, 10*time.Minute)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendAndSaveCodeWithTime send email and save code
|
||||
func (es *EmailService) SendAndSaveCodeWithTime(
|
||||
ctx context.Context, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
|
||||
es.Send(ctx, toEmailAddr, subject, body)
|
||||
err := es.emailRepo.SetCode(ctx, code, codeContent, duration)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send email send
|
||||
func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) {
|
||||
func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) {
|
||||
log.Infof("try to send email to %s", toEmailAddr)
|
||||
ec, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
|
@ -109,13 +133,6 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, co
|
|||
} else {
|
||||
log.Infof("send email to %s success", toEmailAddr)
|
||||
}
|
||||
|
||||
if len(code) > 0 {
|
||||
err = es.emailRepo.SetCode(ctx, code, codeContent)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyUrlExpired email send
|
||||
|
@ -250,41 +267,118 @@ func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl
|
|||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
}
|
||||
|
||||
// TestTemplate send test email template parse
|
||||
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
|
||||
ec, err := es.GetEmailConfig()
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteinfo, err := es.GetSiteGeneral(ctx)
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
templateData := TestTemplateData{
|
||||
SiteName: siteinfo.Name,
|
||||
SiteName: siteInfo.Name,
|
||||
}
|
||||
|
||||
titleBuf := &bytes.Buffer{}
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
title, err = es.parseTemplateData(emailConfig.TestTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("test_title").Parse(ec.TestTitle)
|
||||
body, err = es.parseTemplateData(emailConfig.TestBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email test title template parse error: %s", err)
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
err = tmpl.Execute(titleBuf, templateData)
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewAnswerTemplate new answer template
|
||||
func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email test body template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
tmpl, err = template.New("test_body").Parse(ec.TestBody)
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("test_body template parse error: %s", err)
|
||||
return
|
||||
}
|
||||
err = tmpl.Execute(bodyBuf, templateData)
|
||||
templateData := &schema.NewAnswerTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
DisplayName: raw.AnswerUserDisplayName,
|
||||
QuestionTitle: raw.QuestionTitle,
|
||||
AnswerUrl: fmt.Sprintf("%s/questions/%s/%s", siteInfo.SiteUrl, raw.QuestionID, raw.AnswerID),
|
||||
AnswerSummary: raw.AnswerSummary,
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.NewAnswerTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.NewAnswerBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
// NewCommentTemplate new comment template
|
||||
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := &schema.NewCommentTemplateData{
|
||||
SiteName: siteInfo.Name,
|
||||
DisplayName: raw.CommentUserDisplayName,
|
||||
QuestionTitle: raw.QuestionTitle,
|
||||
CommentSummary: raw.CommentSummary,
|
||||
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
|
||||
}
|
||||
if len(raw.AnswerID) > 0 {
|
||||
templateData.CommentUrl = fmt.Sprintf("%s/questions/%s/%s?commentId=%s", siteInfo.SiteUrl, raw.QuestionID,
|
||||
raw.AnswerID, raw.CommentID)
|
||||
} else {
|
||||
templateData.CommentUrl = fmt.Sprintf("%s/questions/%s?commentId=%s", siteInfo.SiteUrl,
|
||||
raw.QuestionID, raw.CommentID)
|
||||
}
|
||||
templateData.SiteName = siteInfo.Name
|
||||
|
||||
title, err = es.parseTemplateData(emailConfig.NewCommentTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
body, err = es.parseTemplateData(emailConfig.NewCommentBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) parseTemplateData(templateContent string, templateData interface{}) (parsedData string, err error) {
|
||||
parsedDataBuf := &bytes.Buffer{}
|
||||
tmpl, err := template.New("").Parse(templateContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = tmpl.Execute(parsedDataBuf, templateData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parsedDataBuf.String(), nil
|
||||
}
|
||||
|
||||
func (es *EmailService) GetEmailConfig() (ec *EmailConfig, err error) {
|
||||
|
|
|
@ -236,7 +236,7 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.emailService.Send(ctx, req.TestEmailRecipient, title, body, "", "")
|
||||
go s.emailService.SendAndSaveCode(ctx, req.TestEmailRecipient, title, body, "", "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -168,7 +168,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
go us.emailService.Send(ctx, req.Email, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
|
||||
return code, nil
|
||||
}
|
||||
|
||||
|
@ -333,7 +333,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
|
@ -382,7 +382,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -514,7 +514,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
}
|
||||
log.Infof("send email confirmation %s", verifyEmailURL)
|
||||
|
||||
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||
go us.emailService.SendAndSaveCode(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -598,6 +598,25 @@ func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankin
|
|||
return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil
|
||||
}
|
||||
|
||||
// UserUnsubscribeEmailNotification user unsubscribe email notification
|
||||
func (us *UserService) UserUnsubscribeEmailNotification(
|
||||
ctx context.Context, req *schema.UserUnsubscribeEmailNotificationReq) (err error) {
|
||||
data := &schema.EmailCodeContent{}
|
||||
err = data.FromJSONString(req.Content)
|
||||
if err != nil || len(data.UserID) == 0 {
|
||||
return errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
return us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, schema.NoticeStatusOff)
|
||||
}
|
||||
|
||||
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
|
||||
userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
|
||||
rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package encryption
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// MD5 return md5 hash
|
||||
func MD5(data string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
Loading…
Reference in New Issue