Merge branch 'feat/1.1.2/notification' into test

This commit is contained in:
LinkinStars 2023-08-22 12:02:42 +08:00
commit 7a036f2118
31 changed files with 1530 additions and 412 deletions

View File

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

View File

@ -5166,29 +5166,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/email/notification": {
"put": {
"description": "unsubscribe email notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe email notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/email/verification": {
"post": {
"description": "UserVerifyEmail",
@ -5526,14 +5503,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 +5520,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 +5570,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.UserNoticeSetResp"
"$ref": "#/definitions/schema.GetUserNotificationConfigResp"
}
}
}
@ -5577,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": [
@ -6198,6 +6235,15 @@ const docTemplate = `{
}
},
"definitions": {
"constant.NotificationChannelKey": {
"type": "string",
"enum": [
"email"
],
"x-enum-varnames": [
"EmailChannel"
]
},
"constant.Privilege": {
"type": "object",
"properties": {
@ -6301,6 +6347,9 @@ const docTemplate = `{
"type": "string",
"maxLength": 30
},
"login_required": {
"type": "boolean"
},
"name": {
"type": "string",
"maxLength": 30
@ -7595,6 +7644,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 +7778,17 @@ const docTemplate = `{
}
}
},
"schema.NotificationChannelConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"key": {
"$ref": "#/definitions/constant.NotificationChannelKey"
}
}
},
"schema.NotificationClearIDRequest": {
"type": "object",
"properties": {
@ -9103,6 +9186,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 +9488,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": {
@ -9510,6 +9600,18 @@ const docTemplate = `{
}
}
},
"schema.UserUnsubscribeNotificationReq": {
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "string",
"maxLength": 500
}
}
},
"schema.VoteReq": {
"type": "object",
"required": [

View File

@ -5154,29 +5154,6 @@
}
}
},
"/answer/api/v1/user/email/notification": {
"put": {
"description": "unsubscribe email notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "unsubscribe email notification",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/user/email/verification": {
"post": {
"description": "UserVerifyEmail",
@ -5514,14 +5491,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 +5508,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 +5558,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.UserNoticeSetResp"
"$ref": "#/definitions/schema.GetUserNotificationConfigResp"
}
}
}
@ -5565,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": [
@ -6186,6 +6223,15 @@
}
},
"definitions": {
"constant.NotificationChannelKey": {
"type": "string",
"enum": [
"email"
],
"x-enum-varnames": [
"EmailChannel"
]
},
"constant.Privilege": {
"type": "object",
"properties": {
@ -6289,6 +6335,9 @@
"type": "string",
"maxLength": 30
},
"login_required": {
"type": "boolean"
},
"name": {
"type": "string",
"maxLength": 30
@ -7583,6 +7632,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 +7766,17 @@
}
}
},
"schema.NotificationChannelConfig": {
"type": "object",
"properties": {
"enable": {
"type": "boolean"
},
"key": {
"$ref": "#/definitions/constant.NotificationChannelKey"
}
}
},
"schema.NotificationClearIDRequest": {
"type": "object",
"properties": {
@ -9091,6 +9174,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 +9476,6 @@
}
}
},
"schema.UserNoticeSetRequest": {
"type": "object",
"properties": {
"notice_switch": {
"type": "boolean"
}
}
},
"schema.UserNoticeSetResp": {
"type": "object",
"properties": {
"notice_switch": {
"type": "boolean"
}
}
},
"schema.UserRankingResp": {
"type": "object",
"properties": {
@ -9498,6 +9588,18 @@
}
}
},
"schema.UserUnsubscribeNotificationReq": {
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"type": "string",
"maxLength": 500
}
}
},
"schema.VoteReq": {
"type": "object",
"required": [

View File

@ -1,4 +1,10 @@
definitions:
constant.NotificationChannelKey:
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.NotificationChannelKey'
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:
@ -2316,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:
@ -5478,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:
@ -5698,18 +5726,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 +5741,56 @@ 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/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:

View File

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

View File

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

View File

@ -39,6 +39,19 @@ const (
NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer"
)
type NotificationChannelKey string
type NotificationSource string
const (
InboxSource NotificationSource = "inbox"
AllNewQuestionSource NotificationSource = "all_new_question"
AllNewQuestionForFollowingTagsSource NotificationSource = "all_new_question_for_following_tags"
)
const (
EmailChannel NotificationChannelKey = "email"
)
var (
NotificationMsgTypeMapping = map[string]int{
NotificationUpdateQuestion: 1,

View File

@ -13,6 +13,7 @@ import (
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_notification_config"
"github.com/answerdev/answer/pkg/checker"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@ -21,11 +22,12 @@ import (
// UserController user controller
type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
emailService *export.EmailService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
emailService *export.EmailService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
userNotificationConfigService *user_notification_config.UserNotificationConfigService
}
// NewUserController new controller
@ -35,13 +37,15 @@ func NewUserController(
actionService *action.CaptchaService,
emailService *export.EmailService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
userNotificationConfigService *user_notification_config.UserNotificationConfigService,
) *UserController {
return &UserController{
authService: authService,
userService: userService,
actionService: actionService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
authService: authService,
userService: userService,
actionService: actionService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
userNotificationConfigService: userNotificationConfigService,
}
}
@ -488,25 +492,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.userNotificationConfigService.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.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UserChangeEmailSendCode send email to the user email then change their email
@ -608,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
}
@ -629,7 +649,7 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
return
}
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
err := uc.userService.UserUnsubscribeNotification(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

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

View File

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

View File

@ -0,0 +1,11 @@
package migrations
import (
"context"
"github.com/answerdev/answer/internal/entity"
"xorm.io/xorm"
)
func addNoticeConfig(ctx context.Context, x *xorm.Engine) error {
return x.Context(ctx).Sync(new(entity.UserNotificationConfig))
}

View File

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

View File

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

View File

@ -114,7 +114,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *
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) {
@ -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

View File

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

View File

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

View File

@ -0,0 +1,132 @@
package schema
import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
)
type NotificationChannelConfig struct {
Key constant.NotificationChannelKey `json:"key"`
Enable bool `json:"enable"`
}
type NotificationChannels []*NotificationChannelConfig
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
}
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)
} else {
newList = append(newList, &NotificationChannelConfig{
Key: ch,
})
}
}
*n = newList
}
func (n *NotificationChannels) CheckEnable(ch constant.NotificationChannelKey) bool {
if n == nil {
return false
}
for _, item := range *n {
if item.Key == ch {
return item.Enable
}
}
return false
}
func (n *NotificationChannels) ToJsonString() string {
data, _ := json.Marshal(n)
return string(data)
}
type NotificationConfig struct {
Inbox NotificationChannels `json:"inbox"`
AllNewQuestion NotificationChannels `json:"all_new_question"`
AllNewQuestionForFollowingTags NotificationChannels `json:"all_new_question_for_following_tags"`
}
func (n *NotificationConfig) ToJsonString() string {
data, _ := json.Marshal(n)
return string(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
}
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.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.NotificationChannelKey) bool {
switch source {
case constant.InboxSource:
return n.Inbox.CheckEnable(channel)
case constant.AllNewQuestionSource:
return n.AllNewQuestion.CheckEnable(channel)
case constant.AllNewQuestionForFollowingTagsSource:
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
}

View File

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

View File

@ -25,26 +25,26 @@ import (
"github.com/answerdev/answer/pkg/encryption"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
// AnswerService user service
type AnswerService struct {
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
func NewAnswerService(
@ -61,23 +61,25 @@ func NewAnswerService(
emailService *export.EmailService,
roleService *role.UserRoleRelService,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *AnswerService {
return &AnswerService{
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -604,7 +606,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
msg.NotificationAction = constant.NotificationAnswerTheQuestion
as.notificationQueueService.Send(ctx, msg)
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
receiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {
log.Error(err)
return
@ -613,37 +615,23 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
log.Warnf("user %s not found", questionUserID)
return
}
if userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 {
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewAnswerTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
AnswerID: answerID,
AnswerSummary: answerSummary,
UnsubscribeCode: encryption.MD5(userInfo.Pass),
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
}
answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
if answerUser != nil {
rawData.AnswerUserDisplayName = answerUser.DisplayName
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: userInfo.EMail,
UserID: userInfo.ID,
}
// If receiver has set language, use it to send email.
if len(userInfo.Language) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language))
}
title, body, err := as.emailService.NewAnswerTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
go as.emailService.SendAndSaveCodeWithTime(
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
externalNotificationMsg.NewAnswerTemplateRawData = rawData
as.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

View File

@ -22,7 +22,6 @@ import (
"github.com/answerdev/answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
@ -58,15 +57,16 @@ func (c *CommentQuery) GetOrderBy() string {
// CommentService user service
type CommentService struct {
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
// NewCommentService new comment service
@ -79,18 +79,20 @@ func NewCommentService(
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *CommentService {
return &CommentService{
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -474,6 +476,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
if questionUserID == commentUserID {
return
}
// send internal notification
msg := &schema.NotificationMsg{
ReceiverUserID: questionUserID,
TriggerUserID: commentUserID,
@ -484,6 +487,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
msg.NotificationAction = constant.NotificationCommentQuestion
cs.notificationQueueService.Send(ctx, msg)
// send external notification
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {
log.Error(err)
@ -493,10 +497,12 @@ 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 {
return
}
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
@ -508,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,
@ -533,6 +523,8 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
if answerUserID == commentUserID {
return
}
// Send internal notification.
msg := &schema.NotificationMsg{
ReceiverUserID: answerUserID,
TriggerUserID: commentUserID,
@ -543,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)
@ -552,10 +545,11 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
log.Warnf("user %s not found", answerUserID)
return
}
if receiverUserInfo.NoticeStatus == schema.NoticeStatusOff || len(receiverUserInfo.EMail) == 0 {
return
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewCommentTemplateRawData{
QuestionTitle: questionTitle,
QuestionID: questionID,
@ -568,24 +562,8 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
if commentUser != nil {
rawData.CommentUserDisplayName = commentUser.DisplayName
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: receiverUserInfo.EMail,
UserID: receiverUserInfo.ID,
}
// If receiver has set language, use it to send email.
if len(receiverUserInfo.Language) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(receiverUserInfo.Language))
}
title, body, err := cs.emailService.NewCommentTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
go cs.emailService.SendAndSaveCodeWithTime(
ctx, receiverUserInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
externalNotificationMsg.NewCommentTemplateRawData = rawData
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"strings"
"time"
@ -32,7 +33,6 @@ import (
"github.com/answerdev/answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"golang.org/x/net/context"
)
@ -41,19 +41,21 @@ import (
// QuestionService user service
type QuestionService struct {
questionRepo questioncommon.QuestionRepo
tagCommon *tagcommon.TagCommonService
questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService
emailService *export.EmailService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
siteInfoService siteinfo_common.SiteInfoCommonService
questionRepo questioncommon.QuestionRepo
tagCommon *tagcommon.TagCommonService
questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService
emailService *export.EmailService
notificationQueueService notice_queue.NotificationQueueService
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
activityQueueService activity_queue.ActivityQueueService
siteInfoService siteinfo_common.SiteInfoCommonService
newQuestionNotificationService *notification.ExternalNotificationService
}
func NewQuestionService(
@ -68,23 +70,27 @@ func NewQuestionService(
answerActivityService *activity.AnswerActivityService,
emailService *export.EmailService,
notificationQueueService notice_queue.NotificationQueueService,
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
siteInfoService siteinfo_common.SiteInfoCommonService,
newQuestionNotificationService *notification.ExternalNotificationService,
) *QuestionService {
return &QuestionService{
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
emailService: emailService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
siteInfoService: siteInfoService,
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
emailService: emailService,
notificationQueueService: notificationQueueService,
externalNotificationQueueService: externalNotificationQueueService,
activityQueueService: activityQueueService,
siteInfoService: siteInfoService,
newQuestionNotificationService: newQuestionNotificationService,
}
}
@ -241,12 +247,12 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
if !req.QuestionPermission.CanUseReservedTag {
taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
taglist, err := qs.AddQuestionCheckTags(ctx, tags)
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(taglist, ","))
if err != nil {
@ -296,7 +302,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
Title: question.Title,
}
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, tags)
if err != nil {
return nil, err
}
@ -326,6 +332,9 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
RevisionID: revisionID,
})
qs.externalNotificationQueueService.Send(ctx,
schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, tags))
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
return
}
@ -640,39 +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 userInfo.NoticeStatus == schema.NoticeStatusOff || len(userInfo.EMail) == 0 {
return
externalNotificationMsg := &schema.ExternalNotificationMsg{
ReceiverUserID: receiverUserInfo.ID,
ReceiverEmail: receiverUserInfo.EMail,
ReceiverLang: receiverUserInfo.Language,
}
rawData := &schema.NewInviteAnswerTemplateRawData{
InviterDisplayName: inviter.DisplayName,
QuestionTitle: questionTitle,
QuestionID: questionID,
UnsubscribeCode: encryption.MD5(userInfo.Pass),
UnsubscribeCode: encryption.MD5(receiverUserInfo.Pass),
}
codeContent := &schema.EmailCodeContent{
SourceType: schema.UnsubscribeSourceType,
Email: userInfo.EMail,
UserID: userInfo.ID,
}
// If receiver has set language, use it to send email.
if len(userInfo.Language) > 0 {
ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language))
}
title, body, err := qs.emailService.NewInviteAnswerTemplate(ctx, rawData)
if err != nil {
log.Error(err)
return
}
go qs.emailService.SendAndSaveCodeWithTime(
ctx, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 7*24*time.Hour)
externalNotificationMsg.NewInviteAnswerTemplateRawData = rawData
qs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
}

View File

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

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/user_notification_config"
"time"
"github.com/answerdev/answer/internal/base/handler"
@ -28,19 +30,18 @@ import (
"golang.org/x/crypto/bcrypt"
)
// UserRepo user repository
// 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,
@ -52,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,
}
}
@ -93,8 +96,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 +347,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,25 +464,6 @@ 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)
if err != nil {
return nil, err
}
if !has {
return nil, errors.BadRequest(reason.UserNotFound)
}
if noticeSwitch {
userInfo.NoticeStatus = schema.NoticeStatusOn
} else {
userInfo.NoticeStatus = schema.NoticeStatusOff
}
err = us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, userInfo.NoticeStatus)
return &schema.UserNoticeSetResp{NoticeSwitch: noticeSwitch}, err
}
func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.UserLoginResp, err error) {
data := &schema.EmailCodeContent{}
err = data.FromJSONString(req.Content)
@ -720,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,