feat(notification): Email notice when a user answers or comment

This commit is contained in:
LinkinStar 2022-12-27 17:42:23 +08:00
parent 27e84f9ea8
commit 81f914d72f
13 changed files with 436 additions and 76 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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:"-"`
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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
}

View File

@ -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)

13
pkg/encryption/md5.go Normal file
View File

@ -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))
}