From 6a3d2f95d21666950273755004be90ef03217a42 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 21 Aug 2023 15:07:30 +0800 Subject: [PATCH 1/2] feat(notification): add user notification config API --- docs/docs.go | 125 ++++++++++++++++---- docs/swagger.json | 125 ++++++++++++++++---- docs/swagger.yaml | 93 +++++++++++---- internal/base/constant/notification.go | 13 ++ internal/controller/user_controller.go | 35 ++++-- internal/entity/user_entity.go | 1 + internal/migrations/migrations.go | 1 + internal/migrations/v15.go | 13 ++ internal/repo/user/user_repo.go | 10 ++ internal/router/answer_api_router.go | 3 +- internal/schema/user_notification_schema.go | 108 +++++++++++++++++ internal/schema/user_schema.go | 9 -- internal/service/answer_service.go | 3 +- internal/service/comment/comment_service.go | 8 +- internal/service/question_service.go | 4 +- internal/service/user_common/user.go | 1 + internal/service/user_service.go | 43 ++++--- 17 files changed, 481 insertions(+), 114 deletions(-) create mode 100644 internal/migrations/v15.go create mode 100644 internal/schema/user_notification_schema.go diff --git a/docs/docs.go b/docs/docs.go index 9c1519b1..59583cb6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5526,14 +5526,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/notice/set": { - "post": { + "/answer/api/v1/user/notification/config": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserNoticeSet", + "description": "update user's notification config", "consumes": [ "application/json" ], @@ -5543,18 +5543,44 @@ const docTemplate = `{ "tags": [ "User" ], - "summary": "UserNoticeSet", + "summary": "update user's notification config", "parameters": [ { - "description": "UserNoticeSetRequest", + "description": "UpdateUserNotificationConfigReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserNoticeSetRequest" + "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" } } ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user's notification config", "responses": { "200": { "description": "OK", @@ -5567,7 +5593,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.UserNoticeSetResp" + "$ref": "#/definitions/schema.GetUserNotificationConfigResp" } } } @@ -6198,6 +6224,15 @@ const docTemplate = `{ } }, "definitions": { + "constant.NotificationChannel": { + "type": "string", + "enum": [ + "email" + ], + "x-enum-varnames": [ + "EmailChannel" + ] + }, "constant.Privilege": { "type": "object", "properties": { @@ -6301,6 +6336,9 @@ const docTemplate = `{ "type": "string", "maxLength": 30 }, + "login_required": { + "type": "boolean" + }, "name": { "type": "string", "maxLength": 30 @@ -7595,6 +7633,29 @@ const docTemplate = `{ } } }, + "schema.GetUserNotificationConfigResp": { + "type": "object", + "properties": { + "all_new_question": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "all_new_question_for_following_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "inbox": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + } + }, "schema.GetUserPageResp": { "type": "object", "properties": { @@ -7706,6 +7767,17 @@ const docTemplate = `{ } } }, + "schema.NotificationChannelConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "key": { + "$ref": "#/definitions/constant.NotificationChannel" + } + } + }, "schema.NotificationClearIDRequest": { "type": "object", "properties": { @@ -9103,6 +9175,29 @@ const docTemplate = `{ } } }, + "schema.UpdateUserNotificationConfigReq": { + "type": "object", + "properties": { + "all_new_question": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "all_new_question_for_following_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "inbox": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + } + }, "schema.UpdateUserPasswordReq": { "type": "object", "required": [ @@ -9382,22 +9477,6 @@ const docTemplate = `{ } } }, - "schema.UserNoticeSetRequest": { - "type": "object", - "properties": { - "notice_switch": { - "type": "boolean" - } - } - }, - "schema.UserNoticeSetResp": { - "type": "object", - "properties": { - "notice_switch": { - "type": "boolean" - } - } - }, "schema.UserRankingResp": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index cae66cf6..28f555f2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5514,14 +5514,14 @@ } } }, - "/answer/api/v1/user/notice/set": { - "post": { + "/answer/api/v1/user/notification/config": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserNoticeSet", + "description": "update user's notification config", "consumes": [ "application/json" ], @@ -5531,18 +5531,44 @@ "tags": [ "User" ], - "summary": "UserNoticeSet", + "summary": "update user's notification config", "parameters": [ { - "description": "UserNoticeSetRequest", + "description": "UpdateUserNotificationConfigReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserNoticeSetRequest" + "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" } } ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user's notification config", "responses": { "200": { "description": "OK", @@ -5555,7 +5581,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.UserNoticeSetResp" + "$ref": "#/definitions/schema.GetUserNotificationConfigResp" } } } @@ -6186,6 +6212,15 @@ } }, "definitions": { + "constant.NotificationChannel": { + "type": "string", + "enum": [ + "email" + ], + "x-enum-varnames": [ + "EmailChannel" + ] + }, "constant.Privilege": { "type": "object", "properties": { @@ -6289,6 +6324,9 @@ "type": "string", "maxLength": 30 }, + "login_required": { + "type": "boolean" + }, "name": { "type": "string", "maxLength": 30 @@ -7583,6 +7621,29 @@ } } }, + "schema.GetUserNotificationConfigResp": { + "type": "object", + "properties": { + "all_new_question": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "all_new_question_for_following_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "inbox": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + } + }, "schema.GetUserPageResp": { "type": "object", "properties": { @@ -7694,6 +7755,17 @@ } } }, + "schema.NotificationChannelConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "key": { + "$ref": "#/definitions/constant.NotificationChannel" + } + } + }, "schema.NotificationClearIDRequest": { "type": "object", "properties": { @@ -9091,6 +9163,29 @@ } } }, + "schema.UpdateUserNotificationConfigReq": { + "type": "object", + "properties": { + "all_new_question": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "all_new_question_for_following_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + }, + "inbox": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + } + }, "schema.UpdateUserPasswordReq": { "type": "object", "required": [ @@ -9370,22 +9465,6 @@ } } }, - "schema.UserNoticeSetRequest": { - "type": "object", - "properties": { - "notice_switch": { - "type": "boolean" - } - } - }, - "schema.UserNoticeSetResp": { - "type": "object", - "properties": { - "notice_switch": { - "type": "boolean" - } - } - }, "schema.UserRankingResp": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 020550bd..fb75d646 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,10 @@ definitions: + constant.NotificationChannel: + enum: + - email + type: string + x-enum-varnames: + - EmailChannel constant.Privilege: properties: key: @@ -63,6 +69,8 @@ definitions: lang: maxLength: 30 type: string + login_required: + type: boolean name: maxLength: 30 type: string @@ -983,6 +991,21 @@ definitions: activation_url: type: string type: object + schema.GetUserNotificationConfigResp: + properties: + all_new_question: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + all_new_question_for_following_tags: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + inbox: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + type: object schema.GetUserPageResp: properties: avatar: @@ -1064,6 +1087,13 @@ definitions: text: type: string type: object + schema.NotificationChannelConfig: + properties: + enable: + type: boolean + key: + $ref: '#/definitions/constant.NotificationChannel' + type: object schema.NotificationClearIDRequest: properties: id: @@ -2028,6 +2058,21 @@ definitions: required: - language type: object + schema.UpdateUserNotificationConfigReq: + properties: + all_new_question: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + all_new_question_for_following_tags: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + inbox: + items: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: array + type: object schema.UpdateUserPasswordReq: properties: password: @@ -2228,16 +2273,6 @@ definitions: required: - pass type: object - schema.UserNoticeSetRequest: - properties: - notice_switch: - type: boolean - type: object - schema.UserNoticeSetResp: - properties: - notice_switch: - type: boolean - type: object schema.UserRankingResp: properties: staffs: @@ -5698,18 +5733,11 @@ paths: summary: user logout tags: - User - /answer/api/v1/user/notice/set: + /answer/api/v1/user/notification/config: post: consumes: - application/json - description: UserNoticeSet - parameters: - - description: UserNoticeSetRequest - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.UserNoticeSetRequest' + description: get user's notification config produces: - application/json responses: @@ -5720,11 +5748,34 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.UserNoticeSetResp' + $ref: '#/definitions/schema.GetUserNotificationConfigResp' type: object security: - ApiKeyAuth: [] - summary: UserNoticeSet + summary: get user's notification config + tags: + - User + put: + consumes: + - application/json + description: update user's notification config + parameters: + - description: UpdateUserNotificationConfigReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserNotificationConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update user's notification config tags: - User /answer/api/v1/user/password: diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index e4d193e6..55ab39ca 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -39,6 +39,19 @@ const ( NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" ) +type NotificationChannel string +type NotificationSource string + +const ( + InboxChannel NotificationSource = "inbox" + AllNewQuestionChannel NotificationSource = "all_new_question" + AllNewQuestionForFollowingTagsChannel NotificationSource = "all_new_question_for_following_tags" +) + +const ( + EmailChannel NotificationChannel = "email" +) + var ( NotificationMsgTypeMapping = map[string]int{ NotificationUpdateQuestion: 1, diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index d286b3ca..dfd16f33 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -488,25 +488,40 @@ func (uc *UserController) UserRegisterCaptcha(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// UserNoticeSet godoc -// @Summary UserNoticeSet -// @Description UserNoticeSet +// GetUserNotificationConfig get user's notification config +// @Summary get user's notification config +// @Description get user's notification config // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.UserNoticeSetRequest true "UserNoticeSetRequest" -// @Success 200 {object} handler.RespBody{data=schema.UserNoticeSetResp} -// @Router /answer/api/v1/user/notice/set [post] -func (uc *UserController) UserNoticeSet(ctx *gin.Context) { - req := &schema.UserNoticeSetRequest{} +// @Success 200 {object} handler.RespBody{data=schema.GetUserNotificationConfigResp} +// @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) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateUserNotificationConfig update user's notification config +// @Summary update user's notification config +// @Description update user's notification config +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateUserNotificationConfigReq true "UpdateUserNotificationConfigReq" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/user/notification/config [put] +func (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) { + req := &schema.UpdateUserNotificationConfigReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := uc.userService.UserNoticeSet(ctx, req.UserID, req.NoticeSwitch) - handler.HandleResponse(ctx, err, resp) + err := uc.userService.UpdateUserNotificationConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) } // UserChangeEmailSendCode send email to the user email then change their email diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index 5d615482..f5c90677 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -46,6 +46,7 @@ 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 diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index ecfff1b7..540e30bc 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -71,6 +71,7 @@ var migrations = []Migration{ NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true), NewMigration("v1.1.0", "add gravatar base url", updateCount, true), NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false), + NewMigration("v1.1.2", "add notification config", addNoticeConfig, false), } func GetMigrations() []Migration { diff --git a/internal/migrations/v15.go b/internal/migrations/v15.go new file mode 100644 index 00000000..7d5f953d --- /dev/null +++ b/internal/migrations/v15.go @@ -0,0 +1,13 @@ +package migrations + +import ( + "context" + "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)) +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index e3b35c15..adc65846 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -120,6 +120,16 @@ 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 { diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 0961cae2..d62c3237 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -215,7 +215,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) r.PUT("/user/interface", a.userController.UserUpdateInterface) - r.POST("/user/notice/set", a.userController.UserNoticeSet) + r.GET("/user/notification/config", a.userController.GetUserNotificationConfig) + r.PUT("/user/notification/config", a.userController.UpdateUserNotificationConfig) r.GET("/user/info/search", a.userController.SearchUserListByName) // vote diff --git a/internal/schema/user_notification_schema.go b/internal/schema/user_notification_schema.go new file mode 100644 index 00000000..e12a7460 --- /dev/null +++ b/internal/schema/user_notification_schema.go @@ -0,0 +1,108 @@ +package schema + +import ( + "encoding/json" + "github.com/answerdev/answer/internal/base/constant" +) + +type NotificationChannelConfig struct { + Key constant.NotificationChannel `json:"key"` + Enable bool `json:"enable"` +} + +type NotificationChannelConfigList []*NotificationChannelConfig + +func (n *NotificationChannelConfigList) Format(sequences []constant.NotificationChannel) { + if n == nil { + *n = make([]*NotificationChannelConfig, 0) + return + } + newList := make([]*NotificationChannelConfig, 0) + mapping := make(map[constant.NotificationChannel]*NotificationChannelConfig) + for _, item := range *n { + mapping[item.Key] = &NotificationChannelConfig{ + Key: item.Key, + Enable: item.Enable, + } + } + for _, ch := range sequences { + if c, ok := mapping[ch]; ok { + newList = append(newList, c) + } else { + newList = append(newList, &NotificationChannelConfig{ + Key: ch, + }) + } + } + *n = newList +} + +func (n *NotificationChannelConfigList) CheckEnable(ch constant.NotificationChannel) bool { + if n == nil { + return false + } + for _, item := range *n { + if item.Key == ch { + return item.Enable + } + } + return false +} + +type NotificationConfig struct { + Inbox NotificationChannelConfigList `json:"inbox"` + AllNewQuestion NotificationChannelConfigList `json:"all_new_question"` + AllNewQuestionForFollowingTags NotificationChannelConfigList `json:"all_new_question_for_following_tags"` +} + +func (n *NotificationConfig) ToJsonString() string { + data, _ := json.Marshal(n) + return string(data) +} + +func NewNotificationConfig(data string) *NotificationConfig { + nc := &NotificationConfig{} + nc.FromJsonString(data) + return nc +} + +func (n *NotificationConfig) FromJsonString(data string) { + if len(data) > 0 { + _ = json.Unmarshal([]byte(data), n) + return + } + n.Inbox = make([]*NotificationChannelConfig, 0) + n.AllNewQuestion = make([]*NotificationChannelConfig, 0) + n.AllNewQuestionForFollowingTags = make([]*NotificationChannelConfig, 0) + return +} + +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}) +} + +func (n *NotificationConfig) CheckEnable( + source constant.NotificationSource, channel constant.NotificationChannel) bool { + switch source { + case constant.InboxChannel: + return n.Inbox.CheckEnable(channel) + case constant.AllNewQuestionChannel: + return n.AllNewQuestion.CheckEnable(channel) + case constant.AllNewQuestionForFollowingTagsChannel: + return n.AllNewQuestionForFollowingTags.CheckEnable(channel) + } + return false +} + +// UpdateUserNotificationConfigReq update user notification config request +type UpdateUserNotificationConfigReq struct { + NotificationConfig + UserID string `json:"-"` +} + +// GetUserNotificationConfigResp get user notification config response +type GetUserNotificationConfigResp struct { + NotificationConfig +} diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 0c2d1fe0..1d61df47 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -293,15 +293,6 @@ func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, return nil, nil } -type UserNoticeSetRequest struct { - NoticeSwitch bool `json:"notice_switch"` - UserID string `json:"-"` -} - -type UserNoticeSetResp struct { - NoticeSwitch bool `json:"notice_switch"` -} - type ActionRecordReq struct { Action string `validate:"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote" form:"action"` IP string `json:"-"` diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index b8462551..542c04b2 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -613,7 +613,8 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, log.Warnf("user %s not found", questionUserID) return } - if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 { + if len(userInfo.EMail) == 0 || + schema.NewNotificationConfig(userInfo.NoticeConfig).CheckEnable(constant.InboxChannel, constant.EmailChannel) { return } diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 83970652..6ec6d5fb 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -493,7 +493,9 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest log.Warnf("user %s not found", questionUserID) return } - if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 { + if len(receiverUserInfo.EMail) == 0 || + schema.NewNotificationConfig(receiverUserInfo.NoticeConfig). + CheckEnable(constant.InboxChannel, constant.EmailChannel) { return } @@ -552,7 +554,9 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, log.Warnf("user %s not found", answerUserID) return } - if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 { + if len(receiverUserInfo.EMail) == 0 || + schema.NewNotificationConfig(receiverUserInfo.NoticeConfig). + CheckEnable(constant.InboxChannel, constant.EmailChannel) { return } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 6fdd3852..ce8c13e3 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -645,7 +645,9 @@ func (qs *QuestionService) notificationInviteUser( log.Warnf("user %s not found", userID) return } - if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 { + if len(userInfo.EMail) == 0 || + schema.NewNotificationConfig(userInfo.NoticeConfig). + CheckEnable(constant.InboxChannel, constant.EmailChannel) { return } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index c1886041..aaf5a097 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -27,6 +27,7 @@ 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 diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 776b8765..ead6c51c 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -28,8 +28,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -// UserRepo user repository - // UserService user service type UserService struct { userCommonService *usercommon.UserCommon @@ -93,8 +91,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st } func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( - resp *schema.GetOtherUserInfoByUsernameResp, err error, -) { + resp *schema.GetOtherUserInfoByUsernameResp, err error) { userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) if err != nil { return nil, err @@ -345,14 +342,6 @@ func (us *UserService) formatUserInfoForUpdateInfo( return userInfo } -func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) { - _, has, err := us.userRepo.GetByEmail(ctx, email) - if err != nil { - return false, err - } - return has, nil -} - // UserUpdateInterface update user interface func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) { if !translator.CheckLanguageIsValid(req.Language) { @@ -470,23 +459,31 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e return nil } -func (us *UserService) UserNoticeSet(ctx context.Context, userID string, noticeSwitch bool) ( - resp *schema.UserNoticeSetResp, err error, -) { - userInfo, has, err := us.userRepo.GetByUserID(ctx, userID) +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 !has { + if !exist { return nil, errors.BadRequest(reason.UserNotFound) } - if noticeSwitch { - userInfo.NoticeStatus = schema.NoticeStatusOn - } else { - userInfo.NoticeStatus = schema.NoticeStatusOff + 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 } - err = us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, userInfo.NoticeStatus) - return &schema.UserNoticeSetResp{NoticeSwitch: noticeSwitch}, 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) { From 2bd8f7f771b2192a86616baa491e6a8c7a95f1f4 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 22 Aug 2023 11:59:33 +0800 Subject: [PATCH 2/2] feat(notification): add new question notification --- cmd/wire_gen.go | 24 +-- docs/docs.go | 73 +++++---- docs/swagger.json | 73 +++++---- docs/swagger.yaml | 49 +++--- i18n/en_US.yaml | 5 + internal/base/constant/email_tpl_key.go | 3 + internal/base/constant/notification.go | 10 +- internal/controller/user_controller.go | 43 +++--- internal/entity/user_entity.go | 1 - .../entity/user_notification_config_entity.go | 19 +++ internal/migrations/v15.go | 6 +- internal/repo/provider.go | 2 + internal/repo/user/user_repo.go | 10 -- .../user_notification_config_repo.go | 90 +++++++++++ internal/router/answer_api_router.go | 2 +- internal/schema/email_template.go | 25 +++- internal/schema/new_question_queue_schema.go | 32 ++++ internal/schema/user_notification_schema.go | 64 +++++--- internal/schema/user_schema.go | 4 +- internal/service/answer_service.go | 93 +++++------- internal/service/comment/comment_service.go | 104 +++++-------- internal/service/export/email_service.go | 38 ++++- .../external_notification_queue.go | 50 +++++++ .../notification/external_notification.go | 59 ++++++++ .../invite_answer_notification.go | 59 ++++++++ .../notification/new_answer_notification.go | 59 ++++++++ .../notification/new_comment_notification.go | 59 ++++++++ .../notification/new_question_notification.go | 141 ++++++++++++++++++ internal/service/provider.go | 4 + internal/service/question_service.go | 102 ++++++------- internal/service/user_common/user.go | 1 - .../user_notification_config_service.go | 99 ++++++++++++ internal/service/user_service.go | 102 ++++++------- 33 files changed, 1128 insertions(+), 377 deletions(-) create mode 100644 internal/entity/user_notification_config_entity.go create mode 100644 internal/repo/user_notification_config/user_notification_config_repo.go create mode 100644 internal/schema/new_question_queue_schema.go create mode 100644 internal/service/notice_queue/external_notification_queue.go create mode 100644 internal/service/notification/external_notification.go create mode 100644 internal/service/notification/invite_answer_notification.go create mode 100644 internal/service/notification/new_answer_notification.go create mode 100644 internal/service/notification/new_comment_notification.go create mode 100644 internal/service/notification/new_question_notification.go create mode 100644 internal/service/user_notification_config/user_notification_config_service.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index fb1f750d..eae73d48 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -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) diff --git a/docs/docs.go b/docs/docs.go index 59583cb6..1894bcc3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": [ diff --git a/docs/swagger.json b/docs/swagger.json index 28f555f2..dad6cf4e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fb75d646..537c2f6b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 7c7b4cae..a6e7fdff 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -431,6 +431,11 @@ backend: other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" body: other: "{{.QuestionTitle}}

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

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}

\n\n{{.Tags}}

\n\nUnsubscribe" pass_reset: title: other: "[{{.SiteName }}] Password reset" diff --git a/internal/base/constant/email_tpl_key.go b/internal/base/constant/email_tpl_key.go index cb191165..84d30fc0 100644 --- a/internal/base/constant/email_tpl_key.go +++ b/internal/base/constant/email_tpl_key.go @@ -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" ) diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index 55ab39ca..8ebea5d5 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -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 ( diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index dfd16f33..4ad697cf 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -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) } diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index f5c90677..5d615482 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -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 diff --git a/internal/entity/user_notification_config_entity.go b/internal/entity/user_notification_config_entity.go new file mode 100644 index 00000000..4fe5205b --- /dev/null +++ b/internal/entity/user_notification_config_entity.go @@ -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" +} diff --git a/internal/migrations/v15.go b/internal/migrations/v15.go index 7d5f953d..d85dd239 100644 --- a/internal/migrations/v15.go +++ b/internal/migrations/v15.go @@ -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)) } diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 34fe1c1a..100ceb76 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -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, ) diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index adc65846..e3b35c15 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -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 { diff --git a/internal/repo/user_notification_config/user_notification_config_repo.go b/internal/repo/user_notification_config/user_notification_config_repo.go new file mode 100644 index 00000000..15dba36f --- /dev/null +++ b/internal/repo/user_notification_config/user_notification_config_repo.go @@ -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 +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index d62c3237..571e5937 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -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) { diff --git a/internal/schema/email_template.go b/internal/schema/email_template.go index 01cc1667..2884eaa7 100644 --- a/internal/schema/email_template.go +++ b/internal/schema/email_template.go @@ -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 +} diff --git a/internal/schema/new_question_queue_schema.go b/internal/schema/new_question_queue_schema.go new file mode 100644 index 00000000..2bc05431 --- /dev/null +++ b/internal/schema/new_question_queue_schema.go @@ -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 +} diff --git a/internal/schema/user_notification_schema.go b/internal/schema/user_notification_schema.go index e12a7460..bfb640d9 100644 --- a/internal/schema/user_notification_schema.go +++ b/internal/schema/user_notification_schema.go @@ -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 diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 1d61df47..49a8e39e 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -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:"-"` } diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index 542c04b2..52f6adca 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -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) } diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 6ec6d5fb..7756ef21 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -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) { diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 1e8297c7..134a4fb2 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -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 { diff --git a/internal/service/notice_queue/external_notification_queue.go b/internal/service/notice_queue/external_notification_queue.go new file mode 100644 index 00000000..a2c8274c --- /dev/null +++ b/internal/service/notice_queue/external_notification_queue.go @@ -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 +} diff --git a/internal/service/notification/external_notification.go b/internal/service/notification/external_notification.go new file mode 100644 index 00000000..eeaf2e5f --- /dev/null +++ b/internal/service/notification/external_notification.go @@ -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 +} diff --git a/internal/service/notification/invite_answer_notification.go b/internal/service/notification/invite_answer_notification.go new file mode 100644 index 00000000..a84b860a --- /dev/null +++ b/internal/service/notification/invite_answer_notification.go @@ -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) +} diff --git a/internal/service/notification/new_answer_notification.go b/internal/service/notification/new_answer_notification.go new file mode 100644 index 00000000..5e802f6b --- /dev/null +++ b/internal/service/notification/new_answer_notification.go @@ -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) +} diff --git a/internal/service/notification/new_comment_notification.go b/internal/service/notification/new_comment_notification.go new file mode 100644 index 00000000..86cd6f2f --- /dev/null +++ b/internal/service/notification/new_comment_notification.go @@ -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) +} diff --git a/internal/service/notification/new_question_notification.go b/internal/service/notification/new_question_notification.go new file mode 100644 index 00000000..86c8bf32 --- /dev/null +++ b/internal/service/notification/new_question_notification.go @@ -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) +} diff --git a/internal/service/provider.go b/internal/service/provider.go index 53990b91..615f95cf 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -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, ) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index ce8c13e3..3af393e0 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -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) } } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index aaf5a097..c1886041 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -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 diff --git a/internal/service/user_notification_config/user_notification_config_service.go b/internal/service/user_notification_config/user_notification_config_service.go new file mode 100644 index 00000000..f41e22cc --- /dev/null +++ b/internal/service/user_notification_config/user_notification_config_service.go @@ -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 +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index ead6c51c..7d42a3d4 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -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,