feat(notification): add new question notification

This commit is contained in:
LinkinStars 2023-08-22 11:59:33 +08:00
parent 6a3d2f95d2
commit 2bd8f7f771
33 changed files with 1128 additions and 377 deletions

View File

@ -26,7 +26,7 @@ import (
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/meta"
"github.com/answerdev/answer/internal/repo/notification"
notification2 "github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/rank"
@ -41,6 +41,7 @@ import (
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/repo/user_external_login"
"github.com/answerdev/answer/internal/repo/user_notification_config"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
@ -58,7 +59,7 @@ import (
"github.com/answerdev/answer/internal/service/follow"
meta2 "github.com/answerdev/answer/internal/service/meta"
"github.com/answerdev/answer/internal/service/notice_queue"
notification2 "github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/plugin_common"
@ -80,6 +81,7 @@ import (
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/internal/service/user_common"
user_external_login2 "github.com/answerdev/answer/internal/service/user_external_login"
user_notification_config2 "github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
)
@ -127,10 +129,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService, siteInfoCommonService)
userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData)
userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService)
userNotificationConfigRepo := user_notification_config.NewUserNotificationConfigRepo(dataData)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService)
userNotificationConfigService := user_notification_config2.NewUserNotificationConfigService(userRepo, userNotificationConfigRepo)
userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
@ -145,7 +149,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
notificationQueueService := notice_queue.NewNotificationQueueService()
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, activityQueueService)
externalNotificationQueueService := notice_queue.NewNewQuestionNotificationQueueService()
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService)
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)
@ -173,8 +178,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
collectionController := controller.NewCollectionController(collectionService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, activityQueueService, siteInfoCommonService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, activityQueueService)
externalNotificationService := notification.NewExternalNotificationService(userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService)
answerController := controller.NewAnswerController(answerService, rankService, captchaService)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
@ -197,9 +203,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon)
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
notificationRepo := notification2.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService)
notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService)
notificationController := controller.NewNotificationController(notificationService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData)
dashboardController := controller.NewDashboardController(dashboardService)

View File

@ -5166,29 +5166,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/email/notification": {
"put": {
"description": "unsubscribe email notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe email notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/email/verification": {
"post": {
"description": "UserVerifyEmail",
@ -5603,6 +5580,40 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/notification/unsubscribe": {
"put": {
"description": "unsubscribe notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe notification",
"parameters": [
{
"description": "UserUnsubscribeNotificationReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UserUnsubscribeNotificationReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/password": {
"put": {
"security": [
@ -6224,7 +6235,7 @@ const docTemplate = `{
}
},
"definitions": {
"constant.NotificationChannel": {
"constant.NotificationChannelKey": {
"type": "string",
"enum": [
"email"
@ -7774,7 +7785,7 @@ const docTemplate = `{
"type": "boolean"
},
"key": {
"$ref": "#/definitions/constant.NotificationChannel"
"$ref": "#/definitions/constant.NotificationChannelKey"
}
}
},
@ -9589,6 +9600,18 @@ const docTemplate = `{
}
}
},
"schema.UserUnsubscribeNotificationReq": {
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "string",
"maxLength": 500
}
}
},
"schema.VoteReq": {
"type": "object",
"required": [

View File

@ -5154,29 +5154,6 @@
}
}
},
"/answer/api/v1/user/email/notification": {
"put": {
"description": "unsubscribe email notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe email notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/email/verification": {
"post": {
"description": "UserVerifyEmail",
@ -5591,6 +5568,40 @@
}
}
},
"/answer/api/v1/user/notification/unsubscribe": {
"put": {
"description": "unsubscribe notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe notification",
"parameters": [
{
"description": "UserUnsubscribeNotificationReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UserUnsubscribeNotificationReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/password": {
"put": {
"security": [
@ -6212,7 +6223,7 @@
}
},
"definitions": {
"constant.NotificationChannel": {
"constant.NotificationChannelKey": {
"type": "string",
"enum": [
"email"
@ -7762,7 +7773,7 @@
"type": "boolean"
},
"key": {
"$ref": "#/definitions/constant.NotificationChannel"
"$ref": "#/definitions/constant.NotificationChannelKey"
}
}
},
@ -9577,6 +9588,18 @@
}
}
},
"schema.UserUnsubscribeNotificationReq": {
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "string",
"maxLength": 500
}
}
},
"schema.VoteReq": {
"type": "object",
"required": [

View File

@ -1,5 +1,5 @@
definitions:
constant.NotificationChannel:
constant.NotificationChannelKey:
enum:
- email
type: string
@ -1092,7 +1092,7 @@ definitions:
enable:
type: boolean
key:
$ref: '#/definitions/constant.NotificationChannel'
$ref: '#/definitions/constant.NotificationChannelKey'
type: object
schema.NotificationClearIDRequest:
properties:
@ -2351,6 +2351,14 @@ definitions:
required:
- e_mail
type: object
schema.UserUnsubscribeNotificationReq:
properties:
code:
maxLength: 500
type: string
required:
- code
type: object
schema.VoteReq:
properties:
captcha_code:
@ -5513,21 +5521,6 @@ paths:
summary: send email to the user email then change their email
tags:
- User
/answer/api/v1/user/email/notification:
put:
consumes:
- application/json
description: unsubscribe email notification
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
summary: unsubscribe email notification
tags:
- User
/answer/api/v1/user/email/verification:
post:
consumes:
@ -5778,6 +5771,28 @@ paths:
summary: update user's notification config
tags:
- User
/answer/api/v1/user/notification/unsubscribe:
put:
consumes:
- application/json
description: unsubscribe notification
parameters:
- description: UserUnsubscribeNotificationReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UserUnsubscribeNotificationReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
summary: unsubscribe notification
tags:
- User
/answer/api/v1/user/password:
put:
consumes:

View File

@ -431,6 +431,11 @@ backend:
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"
body:
other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
new_question:
title:
other: "[{{.SiteName}}] New question: {{.QuestionTitle}}"
body:
other: "<strong><a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.Tags}}</small><br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
pass_reset:
title:
other: "[{{.SiteName }}] Password reset"

View File

@ -21,4 +21,7 @@ const (
EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title"
EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body"
EmailTplKeyNewQuestionTitle = "email_tpl.new_question.title"
EmailTplKeyNewQuestionBody = "email_tpl.new_question.body"
)

View File

@ -39,17 +39,17 @@ const (
NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer"
)
type NotificationChannel string
type NotificationChannelKey string
type NotificationSource string
const (
InboxChannel NotificationSource = "inbox"
AllNewQuestionChannel NotificationSource = "all_new_question"
AllNewQuestionForFollowingTagsChannel NotificationSource = "all_new_question_for_following_tags"
InboxSource NotificationSource = "inbox"
AllNewQuestionSource NotificationSource = "all_new_question"
AllNewQuestionForFollowingTagsSource NotificationSource = "all_new_question_for_following_tags"
)
const (
EmailChannel NotificationChannel = "email"
EmailChannel NotificationChannelKey = "email"
)
var (

View File

@ -13,6 +13,7 @@ import (
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/answerdev/answer/pkg/checker"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@ -21,11 +22,12 @@ import (
// UserController user controller
type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
emailService *export.EmailService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
emailService *export.EmailService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
userNotificationConfigService *user_notification_config.UserNotificationConfigService
}
// NewUserController new controller
@ -35,13 +37,15 @@ func NewUserController(
actionService *action.CaptchaService,
emailService *export.EmailService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
userNotificationConfigService *user_notification_config.UserNotificationConfigService,
) *UserController {
return &UserController{
authService: authService,
userService: userService,
actionService: actionService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
authService: authService,
userService: userService,
actionService: actionService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
userNotificationConfigService: userNotificationConfigService,
}
}
@ -499,7 +503,7 @@ func (uc *UserController) UserRegisterCaptcha(ctx *gin.Context) {
// @Router /answer/api/v1/user/notification/config [post]
func (uc *UserController) GetUserNotificationConfig(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := uc.userService.GetUserNotificationConfig(ctx, userID)
resp, err := uc.userNotificationConfigService.GetUserNotificationConfig(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
@ -520,7 +524,7 @@ func (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UpdateUserNotificationConfig(ctx, req)
err := uc.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -623,16 +627,17 @@ func (uc *UserController) UserRanking(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// UserUnsubscribeEmailNotification unsubscribe email notification
// @Summary unsubscribe email notification
// @Description unsubscribe email notification
// UserUnsubscribeNotification unsubscribe notification
// @Summary unsubscribe notification
// @Description unsubscribe notification
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserUnsubscribeNotificationReq true "UserUnsubscribeNotificationReq"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/user/email/notification [put]
func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
req := &schema.UserUnsubscribeEmailNotificationReq{}
// @Router /answer/api/v1/user/notification/unsubscribe [put]
func (uc *UserController) UserUnsubscribeNotification(ctx *gin.Context) {
req := &schema.UserUnsubscribeNotificationReq{}
if handler.BindAndCheck(ctx, req) {
return
}
@ -644,7 +649,7 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
return
}
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
err := uc.userService.UserUnsubscribeNotification(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -46,7 +46,6 @@ type User struct {
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
NoticeConfig string `xorm:"not null TEXT notice_config"`
}
// TableName user table name

View File

@ -0,0 +1,19 @@
package entity
import "time"
// UserNotificationConfig user notification config
type UserNotificationConfig struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 INDEX UNIQUE(uk_us) BIGINT(20) INDEX user_id"`
Source string `xorm:"not null default '' INDEX UNIQUE(uk_us) VARCHAR(64) source"`
Channels string `xorm:"not null TEXT channels"`
Enabled bool `xorm:"not null default false BOOL enabled"`
}
// TableName notification table name
func (UserNotificationConfig) TableName() string {
return "user_notification_config"
}

View File

@ -2,12 +2,10 @@ package migrations
import (
"context"
"github.com/answerdev/answer/internal/entity"
"xorm.io/xorm"
)
func addNoticeConfig(ctx context.Context, x *xorm.Engine) error {
type User struct {
NoticeConfig string `xorm:"not null TEXT notice_config"`
}
return x.Context(ctx).Sync(new(User))
return x.Context(ctx).Sync(new(entity.UserNotificationConfig))
}

View File

@ -27,6 +27,7 @@ import (
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/repo/user_external_login"
"github.com/answerdev/answer/internal/repo/user_notification_config"
"github.com/google/wire"
)
@ -73,4 +74,5 @@ var ProviderSetRepo = wire.NewSet(
role.NewPowerRepo,
user_external_login.NewUserExternalLoginRepo,
plugin_config.NewPluginConfigRepo,
user_notification_config.NewUserNotificationConfigRepo,
)

View File

@ -120,16 +120,6 @@ func (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, notic
return nil
}
// UpdateNoticeConfig update notice config
func (ur *userRepo) UpdateNoticeConfig(ctx context.Context, userID string, noticeConfig string) error {
cond := &entity.User{NoticeConfig: noticeConfig}
_, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("notice_config").Update(cond)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error {
_, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("pass").Update(&entity.User{Pass: pass})
if err != nil {

View File

@ -0,0 +1,90 @@
package user_notification_config
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/segmentfault/pacman/errors"
)
// userNotificationConfigRepo notification repository
type userNotificationConfigRepo struct {
data *data.Data
}
// NewUserNotificationConfigRepo new repository
func NewUserNotificationConfigRepo(data *data.Data) user_notification_config.UserNotificationConfigRepo {
return &userNotificationConfigRepo{
data: data,
}
}
// Save save notification config, if existed, update, if not exist, insert
func (ur *userNotificationConfigRepo) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) {
old := &entity.UserNotificationConfig{UserID: uc.UserID, Source: uc.Source}
exist, err := ur.data.DB.Context(ctx).Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
old.Channels = uc.Channels
old.Enabled = uc.Enabled
_, err = ur.data.DB.Context(ctx).ID(old.ID).UseBool("enabled").Cols("channels", "enabled").Update(old)
} else {
_, err = ur.data.DB.Context(ctx).Insert(uc)
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetByUserID get notification config by user id
func (ur *userNotificationConfigRepo) GetByUserID(ctx context.Context, userID string) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&configs)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}
// GetBySource get notification config by source
func (ur *userNotificationConfigRepo) GetBySource(ctx context.Context, source constant.NotificationSource) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).UseBool("enabled").
Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}
// GetByUserIDAndSource get notification config by user id and source
func (ur *userNotificationConfigRepo) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) (
conf *entity.UserNotificationConfig, exist bool, err error) {
config := &entity.UserNotificationConfig{UserID: userID, Source: string(source)}
exist, err = ur.data.DB.Context(ctx).Get(config)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return config, exist, nil
}
// GetByUsersAndSource get notification config by user ids and source
func (ur *userNotificationConfigRepo) GetByUsersAndSource(
ctx context.Context, userIDs []string, source constant.NotificationSource) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).UseBool("enabled").In("user_id", userIDs).
Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}

View File

@ -113,7 +113,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify)
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
routerGroup.PUT("/user/notification/unsubscribe", a.userController.UserUnsubscribeNotification)
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {

View File

@ -1,6 +1,9 @@
package schema
import "encoding/json"
import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
)
const (
AccountActivationSourceType EmailSourceType = "account-activation"
@ -16,8 +19,10 @@ type EmailCodeContent struct {
SourceType EmailSourceType `json:"source_type"`
Email string `json:"e_mail"`
UserID string `json:"user_id"`
// Used for unsubscribe notification
NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"`
// Used for third-party login account binding
BindingKey string `json:"binding_key"`
BindingKey string `json:"binding_key,omitempty"`
}
func (r *EmailCodeContent) ToJSONString() string {
@ -80,3 +85,19 @@ type NewCommentTemplateData struct {
CommentSummary string
UnsubscribeUrl string
}
type NewQuestionTemplateRawData struct {
QuestionTitle string
QuestionID string
UnsubscribeCode string
Tags []string
TagIDs []string
}
type NewQuestionTemplateData struct {
SiteName string
QuestionTitle string
QuestionUrl string
Tags string
UnsubscribeUrl string
}

View File

@ -0,0 +1,32 @@
package schema
import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/uid"
)
type ExternalNotificationMsg struct {
ReceiverUserID string `json:"receiver_user_id"`
ReceiverEmail string `json:"receiver_email"`
ReceiverLang string `json:"receiver_lang"`
NewAnswerTemplateRawData *NewAnswerTemplateRawData `json:"new_answer_template_raw_data,omitempty"`
NewInviteAnswerTemplateRawData *NewInviteAnswerTemplateRawData `json:"new_invite_answer_template_raw_data,omitempty"`
NewCommentTemplateRawData *NewCommentTemplateRawData `json:"new_comment_template_raw_data,omitempty"`
NewQuestionTemplateRawData *NewQuestionTemplateRawData `json:"new_question_template_raw_data,omitempty"`
}
func CreateNewQuestionNotificationMsg(questionID, questionTitle string, tags []*entity.Tag) *ExternalNotificationMsg {
questionID = uid.DeShortID(questionID)
msg := &ExternalNotificationMsg{
NewQuestionTemplateRawData: &NewQuestionTemplateRawData{
QuestionID: questionID,
QuestionTitle: questionTitle,
},
}
for _, tag := range tags {
msg.NewQuestionTemplateRawData.Tags = append(msg.NewQuestionTemplateRawData.Tags, tag.SlugName)
msg.NewQuestionTemplateRawData.TagIDs = append(msg.NewQuestionTemplateRawData.TagIDs, tag.ID)
}
return msg
}

View File

@ -3,28 +3,35 @@ package schema
import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
)
type NotificationChannelConfig struct {
Key constant.NotificationChannel `json:"key"`
Enable bool `json:"enable"`
Key constant.NotificationChannelKey `json:"key"`
Enable bool `json:"enable"`
}
type NotificationChannelConfigList []*NotificationChannelConfig
type NotificationChannels []*NotificationChannelConfig
func (n *NotificationChannelConfigList) Format(sequences []constant.NotificationChannel) {
func NewNotificationChannelsFormJson(jsonStr string) NotificationChannels {
var list NotificationChannels
_ = json.Unmarshal([]byte(jsonStr), &list)
return list
}
func (n *NotificationChannels) Format(sequences []constant.NotificationChannelKey) {
if n == nil {
*n = make([]*NotificationChannelConfig, 0)
return
}
newList := make([]*NotificationChannelConfig, 0)
mapping := make(map[constant.NotificationChannel]*NotificationChannelConfig)
mapping := make(map[constant.NotificationChannelKey]*NotificationChannelConfig)
for _, item := range *n {
mapping[item.Key] = &NotificationChannelConfig{
Key: item.Key,
Enable: item.Enable,
}
}
newList := make([]*NotificationChannelConfig, 0)
for _, ch := range sequences {
if c, ok := mapping[ch]; ok {
newList = append(newList, c)
@ -37,7 +44,7 @@ func (n *NotificationChannelConfigList) Format(sequences []constant.Notification
*n = newList
}
func (n *NotificationChannelConfigList) CheckEnable(ch constant.NotificationChannel) bool {
func (n *NotificationChannels) CheckEnable(ch constant.NotificationChannelKey) bool {
if n == nil {
return false
}
@ -49,10 +56,15 @@ func (n *NotificationChannelConfigList) CheckEnable(ch constant.NotificationChan
return false
}
func (n *NotificationChannels) ToJsonString() string {
data, _ := json.Marshal(n)
return string(data)
}
type NotificationConfig struct {
Inbox NotificationChannelConfigList `json:"inbox"`
AllNewQuestion NotificationChannelConfigList `json:"all_new_question"`
AllNewQuestionForFollowingTags NotificationChannelConfigList `json:"all_new_question_for_following_tags"`
Inbox NotificationChannels `json:"inbox"`
AllNewQuestion NotificationChannels `json:"all_new_question"`
AllNewQuestionForFollowingTags NotificationChannels `json:"all_new_question_for_following_tags"`
}
func (n *NotificationConfig) ToJsonString() string {
@ -60,9 +72,21 @@ func (n *NotificationConfig) ToJsonString() string {
return string(data)
}
func NewNotificationConfig(data string) *NotificationConfig {
nc := &NotificationConfig{}
nc.FromJsonString(data)
func NewNotificationConfig(configs []*entity.UserNotificationConfig) NotificationConfig {
nc := NotificationConfig{}
nc.Inbox = make([]*NotificationChannelConfig, 0)
nc.AllNewQuestion = make([]*NotificationChannelConfig, 0)
nc.AllNewQuestionForFollowingTags = make([]*NotificationChannelConfig, 0)
for _, item := range configs {
switch item.Source {
case string(constant.InboxSource):
nc.Inbox = NewNotificationChannelsFormJson(item.Channels)
case string(constant.AllNewQuestionSource):
nc.AllNewQuestion = NewNotificationChannelsFormJson(item.Channels)
case string(constant.AllNewQuestionForFollowingTagsSource):
nc.AllNewQuestionForFollowingTags = NewNotificationChannelsFormJson(item.Channels)
}
}
return nc
}
@ -78,19 +102,19 @@ func (n *NotificationConfig) FromJsonString(data string) {
}
func (n *NotificationConfig) Format() {
n.Inbox.Format([]constant.NotificationChannel{constant.EmailChannel})
n.AllNewQuestion.Format([]constant.NotificationChannel{constant.EmailChannel})
n.AllNewQuestionForFollowingTags.Format([]constant.NotificationChannel{constant.EmailChannel})
n.Inbox.Format([]constant.NotificationChannelKey{constant.EmailChannel})
n.AllNewQuestion.Format([]constant.NotificationChannelKey{constant.EmailChannel})
n.AllNewQuestionForFollowingTags.Format([]constant.NotificationChannelKey{constant.EmailChannel})
}
func (n *NotificationConfig) CheckEnable(
source constant.NotificationSource, channel constant.NotificationChannel) bool {
source constant.NotificationSource, channel constant.NotificationChannelKey) bool {
switch source {
case constant.InboxChannel:
case constant.InboxSource:
return n.Inbox.CheckEnable(channel)
case constant.AllNewQuestionChannel:
case constant.AllNewQuestionSource:
return n.AllNewQuestion.CheckEnable(channel)
case constant.AllNewQuestionForFollowingTagsChannel:
case constant.AllNewQuestionForFollowingTagsSource:
return n.AllNewQuestionForFollowingTags.CheckEnable(channel)
}
return false

View File

@ -364,8 +364,8 @@ type UserRankingSimpleInfo struct {
Avatar string `json:"avatar"`
}
// UserUnsubscribeEmailNotificationReq user unsubscribe email notification request
type UserUnsubscribeEmailNotificationReq struct {
// UserUnsubscribeNotificationReq user unsubscribe email notification request
type UserUnsubscribeNotificationReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
Content string `json:"-"`
}

View File

@ -25,26 +25,26 @@ import (
"github.com/answerdev/answer/pkg/encryption"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
// AnswerService user service
type AnswerService struct {
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
func NewAnswerService(
@ -61,23 +61,25 @@ func NewAnswerService(
emailService *export.EmailService,
roleService *role.UserRoleRelService,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *AnswerService {
return &AnswerService{
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -604,7 +606,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
msg.NotificationAction = constant.NotificationAnswerTheQuestion
as.notificationQueueService.Send(ctx, msg)
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
receiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {
log.Error(err)
return
@ -613,38 +615,23 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
log.Warnf("user %s not found", questionUserID)
return
}
if len(userInfo.EMail) == 0 ||
schema.NewNotificationConfig(userInfo.NoticeConfig).CheckEnable(constant.InboxChannel, constant.EmailChannel) {
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewAnswerTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
AnswerID: answerID,
AnswerSummary: answerSummary,
UnsubscribeCode: encryption.MD5(userInfo.Pass),
UnsubscribeCode: encryption.MD5(receiverUserInfo.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,
}
// 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 := 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)
externalNotificationMsg.NewAnswerTemplateRawData = rawData
as.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

View File

@ -22,7 +22,6 @@ import (
"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"
)
@ -58,15 +57,16 @@ func (c *CommentQuery) GetOrderBy() string {
// CommentService user service
type CommentService struct {
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
// NewCommentService new comment service
@ -79,18 +79,20 @@ func NewCommentService(
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *CommentService {
return &CommentService{
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -474,6 +476,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
if questionUserID == commentUserID {
return
}
// send internal notification
msg := &schema.NotificationMsg{
ReceiverUserID: questionUserID,
TriggerUserID: commentUserID,
@ -484,6 +487,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
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)
@ -493,12 +497,12 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
log.Warnf("user %s not found", questionUserID)
return
}
if len(receiverUserInfo.EMail) == 0 ||
schema.NewNotificationConfig(receiverUserInfo.NoticeConfig).
CheckEnable(constant.InboxChannel, constant.EmailChannel) {
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
@ -510,24 +514,8 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: receiverUserInfo.EMail,
UserID: receiverUserInfo.ID,
}
// If receiver has set language, use it to send email.
if len(receiverUserInfo.Language) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(receiverUserInfo.Language))
}
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)
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationAnswerComment(ctx context.Context,
@ -535,6 +523,8 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
if answerUserID == commentUserID {
return
}
// Send internal notification.
msg := &schema.NotificationMsg{
ReceiverUserID: answerUserID,
TriggerUserID: commentUserID,
@ -545,6 +535,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
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)
@ -554,12 +545,11 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
log.Warnf("user %s not found", answerUserID)
return
}
if len(receiverUserInfo.EMail) == 0 ||
schema.NewNotificationConfig(receiverUserInfo.NoticeConfig).
CheckEnable(constant.InboxChannel, constant.EmailChannel) {
return
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
@ -572,24 +562,8 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: receiverUserInfo.EMail,
UserID: receiverUserInfo.ID,
}
// If receiver has set language, use it to send email.
if len(receiverUserInfo.Language) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(receiverUserInfo.Language))
}
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)
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) {

View File

@ -6,6 +6,7 @@ import (
"fmt"
"mime"
"os"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
@ -149,7 +150,7 @@ func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (cont
return content
}
func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) {
func (es *EmailService) getSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) {
var (
siteType = "general"
siteInfo *entity.SiteInfo
@ -167,7 +168,7 @@ func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGen
}
func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -183,7 +184,7 @@ func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string
}
func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -197,7 +198,7 @@ func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl stri
}
func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -214,7 +215,7 @@ func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl
// TestTemplate send test email template parse
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -229,7 +230,7 @@ func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, e
// NewAnswerTemplate new answer template
func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -251,7 +252,7 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn
// 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)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -272,7 +273,7 @@ func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema
// NewCommentTemplate new comment template
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.GetSiteGeneral(ctx)
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
@ -297,6 +298,27 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC
return title, body, nil
}
// NewQuestionTemplate new question template
func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.getSiteGeneral(ctx)
if err != nil {
return
}
templateData := &schema.NewQuestionTemplateData{
SiteName: siteInfo.Name,
QuestionTitle: raw.QuestionTitle,
Tags: strings.Join(raw.Tags, ", "),
UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode),
}
templateData.QuestionUrl = fmt.Sprintf("%s/questions/%s", siteInfo.SiteUrl, raw.QuestionID)
lang := handler.GetLangByCtx(ctx)
title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData)
body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData)
return title, body, nil
}
func (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, err error) {
emailConf, err := es.configService.GetStringValue(ctx, "email.config")
if err != nil {

View File

@ -0,0 +1,50 @@
package notice_queue
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
)
type ExternalNotificationQueueService interface {
Send(ctx context.Context, msg *schema.ExternalNotificationMsg)
RegisterHandler(handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error)
}
type externalNotificationQueueService struct {
Queue chan *schema.ExternalNotificationMsg
Handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error
}
func (ns *externalNotificationQueueService) Send(ctx context.Context, msg *schema.ExternalNotificationMsg) {
ns.Queue <- msg
}
func (ns *externalNotificationQueueService) RegisterHandler(
handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error) {
ns.Handler = handler
}
func (ns *externalNotificationQueueService) working() {
go func() {
for msg := range ns.Queue {
log.Debugf("received notification %+v", msg)
if ns.Handler == nil {
log.Warnf("no handler for notification")
continue
}
if err := ns.Handler(context.Background(), msg); err != nil {
log.Error(err)
}
}
}()
}
// NewNewQuestionNotificationQueueService create a new notification queue service
func NewNewQuestionNotificationQueueService() ExternalNotificationQueueService {
ns := &externalNotificationQueueService{}
ns.Queue = make(chan *schema.ExternalNotificationMsg, 128)
ns.working()
return ns
}

View File

@ -0,0 +1,59 @@
package notification
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/notice_queue"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/segmentfault/pacman/log"
)
type ExternalNotificationService struct {
data *data.Data
userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo
followRepo activity_common.FollowRepo
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.ExternalNotificationQueueService
}
func NewExternalNotificationService(
userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo,
followRepo activity_common.FollowRepo,
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.ExternalNotificationQueueService,
) *ExternalNotificationService {
n := &ExternalNotificationService{
userNotificationConfigRepo: userNotificationConfigRepo,
followRepo: followRepo,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
}
notificationQueueService.RegisterHandler(n.Handler)
return n
}
func (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.ExternalNotificationMsg) error {
log.Debugf("try to send external notification %+v", msg)
if msg.NewQuestionTemplateRawData != nil {
return ns.handleNewQuestionNotification(ctx, msg)
}
if msg.NewCommentTemplateRawData != nil {
return ns.handleNewCommentNotification(ctx, msg)
}
if msg.NewAnswerTemplateRawData != nil {
return ns.handleNewAnswerNotification(ctx, msg)
}
if msg.NewInviteAnswerTemplateRawData != nil {
return ns.handleInviteAnswerNotification(ctx, msg)
}
log.Errorf("unknown notification message: %+v", msg)
return nil
}

View File

@ -0,0 +1,59 @@
package notification
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"time"
)
func (ns *ExternalNotificationService) handleInviteAnswerNotification(ctx context.Context,
msg *schema.ExternalNotificationMsg) error {
log.Debugf("try to send invite answer notification %+v", msg)
notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)
if err != nil {
return err
}
if !exist {
return nil
}
channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)
for _, channel := range channels {
if !channel.Enable {
continue
}
switch channel.Key {
case constant.EmailChannel:
ns.sendInviteAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewInviteAnswerTemplateRawData)
}
}
return nil
}
func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx context.Context,
userID, email, lang string, rawData *schema.NewInviteAnswerTemplateRawData) {
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
NotificationSources: []constant.NotificationSource{
constant.InboxSource,
},
Email: email,
UserID: userID,
}
// If receiver has set language, use it to send email.
if len(lang) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang))
}
title, body, err := ns.emailService.NewInviteAnswerTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
ns.emailService.SendAndSaveCodeWithTime(
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
}

View File

@ -0,0 +1,59 @@
package notification
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"time"
)
func (ns *ExternalNotificationService) handleNewAnswerNotification(ctx context.Context,
msg *schema.ExternalNotificationMsg) error {
log.Debugf("try to send new comment notification %+v", msg)
notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)
if err != nil {
return err
}
if !exist {
return nil
}
channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)
for _, channel := range channels {
if !channel.Enable {
continue
}
switch channel.Key {
case constant.EmailChannel:
ns.sendNewAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewAnswerTemplateRawData)
}
}
return nil
}
func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx context.Context,
userID, email, lang string, rawData *schema.NewAnswerTemplateRawData) {
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
NotificationSources: []constant.NotificationSource{
constant.InboxSource,
},
Email: email,
UserID: userID,
}
// If receiver has set language, use it to send email.
if len(lang) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang))
}
title, body, err := ns.emailService.NewAnswerTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
ns.emailService.SendAndSaveCodeWithTime(
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
}

View File

@ -0,0 +1,59 @@
package notification
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"time"
)
func (ns *ExternalNotificationService) handleNewCommentNotification(ctx context.Context,
msg *schema.ExternalNotificationMsg) error {
log.Debugf("try to send new comment notification %+v", msg)
notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource)
if err != nil {
return err
}
if !exist {
return nil
}
channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)
for _, channel := range channels {
if !channel.Enable {
continue
}
switch channel.Key {
case constant.EmailChannel:
ns.sendNewCommentNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewCommentTemplateRawData)
}
}
return nil
}
func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx context.Context,
userID, email, lang string, rawData *schema.NewCommentTemplateRawData) {
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
NotificationSources: []constant.NotificationSource{
constant.InboxSource,
},
Email: email,
UserID: userID,
}
// If receiver has set language, use it to send email.
if len(lang) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang))
}
title, body, err := ns.emailService.NewCommentTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
ns.emailService.SendAndSaveCodeWithTime(
ctx, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
}

View File

@ -0,0 +1,141 @@
package notification
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"time"
)
type NewQuestionSubscriber struct {
UserID string `json:"user_id"`
Channels schema.NotificationChannels `json:"channels"`
}
func (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context.Context,
msg *schema.ExternalNotificationMsg) error {
log.Debugf("try to send new question notification %+v", msg)
subscribers, err := ns.getNewQuestionSubscribers(ctx, msg)
if err != nil {
return err
}
log.Debugf("get subscribers %d for question %s", len(subscribers), msg.NewQuestionTemplateRawData.QuestionID)
for _, subscriber := range subscribers {
for _, channel := range subscriber.Channels {
if !channel.Enable {
continue
}
switch channel.Key {
case constant.EmailChannel:
ns.sendNewQuestionNotificationEmail(ctx, subscriber.UserID, &schema.NewQuestionTemplateRawData{
QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle,
QuestionID: msg.NewQuestionTemplateRawData.QuestionID,
Tags: msg.NewQuestionTemplateRawData.Tags,
})
}
}
}
return nil
}
func (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Context, msg *schema.ExternalNotificationMsg) (
subscribers []*NewQuestionSubscriber, err error) {
subscribersMapping := make(map[string]*NewQuestionSubscriber)
// 1. get all this new question's tags followers
tagsFollowerIDs := make([]string, 0)
followerMapping := make(map[string]bool)
for _, tagID := range msg.NewQuestionTemplateRawData.TagIDs {
userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID)
if err != nil {
log.Error(err)
continue
}
for _, userID := range userIDs {
if _, ok := followerMapping[userID]; ok {
continue
}
followerMapping[userID] = true
tagsFollowerIDs = append(tagsFollowerIDs, userID)
}
}
userNotificationConfigs, err := ns.userNotificationConfigRepo.GetByUsersAndSource(
ctx, tagsFollowerIDs, constant.AllNewQuestionForFollowingTagsSource)
if err != nil {
return nil, err
}
for _, userNotificationConfig := range userNotificationConfigs {
if _, ok := subscribersMapping[userNotificationConfig.UserID]; ok {
continue
}
subscribersMapping[userNotificationConfig.UserID] = &NewQuestionSubscriber{
UserID: userNotificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels),
}
subscribers = append(subscribers, subscribersMapping[userNotificationConfig.UserID])
}
log.Debugf("get %d subscribers from tags", len(subscribersMapping))
// 2. get all new question's followers
notificationConfigs, err := ns.userNotificationConfigRepo.GetBySource(ctx, constant.AllNewQuestionSource)
if err != nil {
return nil, err
}
for _, notificationConfig := range notificationConfigs {
if _, ok := subscribersMapping[notificationConfig.UserID]; ok {
continue
}
if ns.checkSendNewQuestionNotificationEmailLimit(ctx, notificationConfig.UserID) {
continue
}
subscribersMapping[notificationConfig.UserID] = &NewQuestionSubscriber{
UserID: notificationConfig.UserID,
Channels: schema.NewNotificationChannelsFormJson(notificationConfig.Channels),
}
subscribers = append(subscribers, subscribersMapping[notificationConfig.UserID])
}
log.Debugf("get %d subscribers from all new question config", len(subscribers))
return subscribers, nil
}
func (ns *ExternalNotificationService) checkSendNewQuestionNotificationEmailLimit(ctx context.Context, userID string) bool {
// TODO: check if reach send limit
return false
}
func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx context.Context,
userID string, rawData *schema.NewQuestionTemplateRawData) {
userInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID)
if err != nil {
log.Error(err)
return
}
if !exist {
log.Errorf("user %s not exist", userID)
return
}
// 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 := ns.emailService.NewQuestionTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: userInfo.EMail,
UserID: userID,
NotificationSources: []constant.NotificationSource{
constant.AllNewQuestionSource,
constant.AllNewQuestionForFollowingTagsSource,
},
}
ns.emailService.SendAndSaveCodeWithTime(
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour)
}

View File

@ -37,6 +37,7 @@ import (
"github.com/answerdev/answer/internal/service/user_admin"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/google/wire"
)
@ -90,4 +91,7 @@ var ProviderSetService = wire.NewSet(
config.NewConfigService,
notice_queue.NewNotificationQueueService,
activity_queue.NewActivityQueueService,
user_notification_config.NewUserNotificationConfigService,
notification.NewExternalNotificationService,
notice_queue.NewNewQuestionNotificationQueueService,
)

View File

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"strings"
"time"
@ -32,7 +33,6 @@ import (
"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"
)
@ -41,19 +41,21 @@ import (
// QuestionService user service
type QuestionService struct {
questionRepo questioncommon.QuestionRepo
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
emailService *export.EmailService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
siteInfoService siteinfo_common.SiteInfoCommonService
questionRepo questioncommon.QuestionRepo
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
emailService *export.EmailService
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
siteInfoService siteinfo_common.SiteInfoCommonService
newQuestionNotificationService *notification.ExternalNotificationService
}
func NewQuestionService(
@ -68,23 +70,27 @@ func NewQuestionService(
answerActivityService *activity.AnswerActivityService,
emailService *export.EmailService,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
siteInfoService siteinfo_common.SiteInfoCommonService,
newQuestionNotificationService *notification.ExternalNotificationService,
) *QuestionService {
return &QuestionService{
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
emailService: emailService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
siteInfoService: siteInfoService,
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
emailService: emailService,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
siteInfoService: siteInfoService,
newQuestionNotificationService: newQuestionNotificationService,
}
}
@ -241,12 +247,12 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
if !req.QuestionPermission.CanUseReservedTag {
taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
taglist, err := qs.AddQuestionCheckTags(ctx, tags)
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(taglist, ","))
if err != nil {
@ -296,7 +302,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
Title: question.Title,
}
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, tags)
if err != nil {
return nil, err
}
@ -326,6 +332,9 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
RevisionID: revisionID,
})
qs.externalNotificationQueueService.Send(ctx,
schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, tags))
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
return
}
@ -640,41 +649,24 @@ func (qs *QuestionService) notificationInviteUser(
msg.NotificationAction = constant.NotificationInvitedYouToAnswer
qs.notificationQueueService.Send(ctx, msg)
userInfo, ok := invitee[userID]
receiverUserInfo, ok := invitee[userID]
if !ok {
log.Warnf("user %s not found", userID)
return
}
if len(userInfo.EMail) == 0 ||
schema.NewNotificationConfig(userInfo.NoticeConfig).
CheckEnable(constant.InboxChannel, constant.EmailChannel) {
return
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewInviteAnswerTemplateRawData{
InviterDisplayName: inviter.DisplayName,
QuestionTitle: questionTitle,
QuestionID: questionID,
UnsubscribeCode: encryption.MD5(userInfo.Pass),
UnsubscribeCode: encryption.MD5(receiverUserInfo.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)
externalNotificationMsg.NewInviteAnswerTemplateRawData = rawData
qs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
}

View File

@ -27,7 +27,6 @@ type UserRepo interface {
UpdateLastLoginDate(ctx context.Context, userID string) (err error)
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
UpdateNoticeConfig(ctx context.Context, userID string, noticeConfig string) error
UpdateEmail(ctx context.Context, userID, email string) error
UpdateLanguage(ctx context.Context, userID, language string) error
UpdatePass(ctx context.Context, userID, pass string) error

View File

@ -0,0 +1,99 @@
package user_notification_config
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
usercommon "github.com/answerdev/answer/internal/service/user_common"
)
type UserNotificationConfigRepo interface {
Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error)
GetByUserID(ctx context.Context, userID string) ([]*entity.UserNotificationConfig, error)
GetBySource(ctx context.Context, source constant.NotificationSource) ([]*entity.UserNotificationConfig, error)
GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) (
conf *entity.UserNotificationConfig, exist bool, err error)
GetByUsersAndSource(ctx context.Context, userIDs []string, source constant.NotificationSource) (
[]*entity.UserNotificationConfig, error)
}
type UserNotificationConfigService struct {
userRepo usercommon.UserRepo
userNotificationConfigRepo UserNotificationConfigRepo
}
func NewUserNotificationConfigService(
userRepo usercommon.UserRepo,
userNotificationConfigRepo UserNotificationConfigRepo,
) *UserNotificationConfigService {
return &UserNotificationConfigService{
userRepo: userRepo,
userNotificationConfigRepo: userNotificationConfigRepo,
}
}
func (us *UserNotificationConfigService) GetUserNotificationConfig(ctx context.Context, userID string) (
resp *schema.GetUserNotificationConfigResp, err error) {
notificationConfigs, err := us.userNotificationConfigRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
resp = &schema.GetUserNotificationConfigResp{}
resp.NotificationConfig = schema.NewNotificationConfig(notificationConfigs)
resp.Format()
return resp, nil
}
func (us *UserNotificationConfigService) UpdateUserNotificationConfig(
ctx context.Context, req *schema.UpdateUserNotificationConfigReq) (err error) {
req.NotificationConfig.Format()
err = us.userNotificationConfigRepo.Save(ctx,
us.convertToEntity(ctx, req.UserID, constant.InboxSource, req.NotificationConfig.Inbox))
if err != nil {
return err
}
err = us.userNotificationConfigRepo.Save(ctx,
us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionSource, req.NotificationConfig.AllNewQuestion))
if err != nil {
return err
}
err = us.userNotificationConfigRepo.Save(ctx,
us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionForFollowingTagsSource,
req.NotificationConfig.AllNewQuestionForFollowingTags))
if err != nil {
return err
}
return nil
}
func (us *UserNotificationConfigService) convertToEntity(ctx context.Context, userID string,
source constant.NotificationSource, channels schema.NotificationChannels) (c *entity.UserNotificationConfig) {
c = &entity.UserNotificationConfig{
UserID: userID,
Source: string(source),
Channels: channels.ToJsonString(),
}
for _, ch := range channels {
if ch.Enable {
c.Enabled = true
break
}
}
return c
}
func (us *UserNotificationConfigService) CheckEnable(
ctx context.Context, userID string, source constant.NotificationSource,
channel constant.NotificationChannelKey) (enable bool, err error) {
conf, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource(ctx, userID, source)
if err != nil {
return false, err
}
if !exist {
return false, nil
}
notificationChannels := schema.NewNotificationChannelsFormJson(conf.Channels)
return notificationChannels.CheckEnable(channel), nil
}

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/user_notification_config"
"time"
"github.com/answerdev/answer/internal/base/handler"
@ -30,15 +32,16 @@ import (
// UserService user service
type UserService struct {
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
emailService *export.EmailService
authService *auth.AuthService
siteInfoService siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
emailService *export.EmailService
authService *auth.AuthService
siteInfoService siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService
userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo
}
func NewUserService(userRepo usercommon.UserRepo,
@ -50,17 +53,19 @@ func NewUserService(userRepo usercommon.UserRepo,
userRoleService *role.UserRoleRelService,
userCommonService *usercommon.UserCommon,
userExternalLoginService *user_external_login.UserExternalLoginService,
userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo,
) *UserService {
return &UserService{
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userExternalLoginService: userExternalLoginService,
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userExternalLoginService: userExternalLoginService,
userNotificationConfigRepo: userNotificationConfigRepo,
}
}
@ -459,33 +464,6 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
return nil
}
func (us *UserService) GetUserNotificationConfig(ctx context.Context, userID string) (
resp *schema.GetUserNotificationConfigResp, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
resp = &schema.GetUserNotificationConfigResp{}
resp.FromJsonString(userInfo.NoticeConfig)
resp.Format()
return resp, nil
}
func (us *UserService) UpdateUserNotificationConfig(ctx context.Context, req *schema.UpdateUserNotificationConfigReq) (err error) {
_, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.UserNotFound)
}
req.NotificationConfig.Format()
return us.userRepo.UpdateNoticeConfig(ctx, req.UserID, req.NotificationConfig.ToJsonString())
}
func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.UserLoginResp, err error) {
data := &schema.EmailCodeContent{}
err = data.FromJSONString(req.Content)
@ -717,23 +695,37 @@ 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) {
// UserUnsubscribeNotification user unsubscribe email notification
func (us *UserService) UserUnsubscribeNotification(
ctx context.Context, req *schema.UserUnsubscribeNotificationReq) (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
for _, source := range data.NotificationSources {
notificationConfig, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource(
ctx, data.UserID, source)
if err != nil {
return err
}
if !exist {
continue
}
channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels)
// unsubscribe email notification
for _, channel := range channels {
if channel.Key == constant.EmailChannel {
channel.Enable = false
}
}
notificationConfig.Channels = channels.ToJsonString()
if err = us.userNotificationConfigRepo.Save(ctx, notificationConfig); err != nil {
return err
}
}
if !exist {
return errors.BadRequest(reason.UserNotFound)
}
return us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, schema.NoticeStatusOff)
return nil
}
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,