blob: 2bceb629822b4ece696a77556a8fd19c471b80f0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package notificationcommon
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/base/translator"
"github.com/apache/answer/internal/service/siteinfo_common"
"github.com/apache/answer/internal/service/user_external_login"
"github.com/apache/answer/pkg/display"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/notice_queue"
"github.com/apache/answer/internal/service/object_info"
usercommon "github.com/apache/answer/internal/service/user_common"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/plugin"
"github.com/goccy/go-json"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type NotificationRepo interface {
AddNotification(ctx context.Context, notification *entity.Notification) (err error)
GetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error)
ClearUnRead(ctx context.Context, userID string, notificationType int) (err error)
ClearIDUnRead(ctx context.Context, userID string, id string) (err error)
GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error)
UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error)
GetById(ctx context.Context, id string) (*entity.Notification, bool, error)
CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error)
DeleteNotification(ctx context.Context, userID string) (err error)
DeleteUserNotificationConfig(ctx context.Context, userID string) (err error)
}
type NotificationCommon struct {
data *data.Data
notificationRepo NotificationRepo
activityRepo activity_common.ActivityRepo
followRepo activity_common.FollowRepo
userCommon *usercommon.UserCommon
objectInfoService *object_info.ObjService
notificationQueueService notice_queue.NotificationQueueService
userExternalLoginRepo user_external_login.UserExternalLoginRepo
siteInfoService siteinfo_common.SiteInfoCommonService
}
func NewNotificationCommon(
data *data.Data,
notificationRepo NotificationRepo,
userCommon *usercommon.UserCommon,
activityRepo activity_common.ActivityRepo,
followRepo activity_common.FollowRepo,
objectInfoService *object_info.ObjService,
notificationQueueService notice_queue.NotificationQueueService,
userExternalLoginRepo user_external_login.UserExternalLoginRepo,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *NotificationCommon {
notification := &NotificationCommon{
data: data,
notificationRepo: notificationRepo,
activityRepo: activityRepo,
followRepo: followRepo,
userCommon: userCommon,
objectInfoService: objectInfoService,
notificationQueueService: notificationQueueService,
userExternalLoginRepo: userExternalLoginRepo,
siteInfoService: siteInfoService,
}
notificationQueueService.RegisterHandler(notification.AddNotification)
return notification
}
// AddNotification
// need set
// LoginUserID
// Type 1 inbox 2 achievement
// [inbox] Activity
// [achievement] Rank
// ObjectInfo.Title
// ObjectInfo.ObjectID
// ObjectInfo.ObjectType
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) {
if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {
return nil
}
req := &schema.NotificationContent{
TriggerUserID: msg.TriggerUserID,
ReceiverUserID: msg.ReceiverUserID,
ObjectInfo: schema.ObjectInfo{
Title: msg.Title,
ObjectID: uid.DeShortID(msg.ObjectID),
ObjectType: msg.ObjectType,
},
NotificationAction: msg.NotificationAction,
Type: msg.Type,
}
var questionID string // just for notify all followers
var objInfo *schema.SimpleObjectInfo
if msg.ObjectType == constant.BadgeAwardObjectType {
req.ObjectInfo.Title = msg.Title
objectMap := make(map[string]string)
objectMap["badge_id"] = msg.ExtraInfo["badge_id"]
req.ObjectInfo.ObjectMap = objectMap
} else {
objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID)
if err != nil {
log.Error(err)
return err
} else {
req.ObjectInfo.Title = objInfo.Title
questionID = objInfo.QuestionID
objectMap := make(map[string]string)
objectMap["question"] = uid.DeShortID(objInfo.QuestionID)
objectMap["answer"] = uid.DeShortID(objInfo.AnswerID)
objectMap["comment"] = objInfo.CommentID
req.ObjectInfo.ObjectMap = objectMap
}
}
if msg.Type == schema.NotificationTypeAchievement {
notificationInfo, exist, err := ns.notificationRepo.GetByUserIdObjectIdTypeId(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID, req.Type)
if err != nil {
return fmt.Errorf("get by user id object id type id error: %w", err)
}
rank, err := ns.activityRepo.GetUserIDObjectIDActivitySum(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID)
if err != nil {
return fmt.Errorf("get user id object id activity sum error: %w", err)
}
req.Rank = rank
if exist {
//modify notification
updateContent := &schema.NotificationContent{}
err := json.Unmarshal([]byte(notificationInfo.Content), updateContent)
if err != nil {
return fmt.Errorf("unmarshal notification content error: %w", err)
}
updateContent.Rank = rank
content, _ := json.Marshal(updateContent)
notificationInfo.Content = string(content)
err = ns.notificationRepo.UpdateNotificationContent(ctx, notificationInfo)
if err != nil {
return fmt.Errorf("update notification content error: %w", err)
}
return nil
}
}
info := &entity.Notification{}
now := time.Now()
info.UserID = req.ReceiverUserID
info.Type = req.Type
info.IsRead = schema.NotificationNotRead
info.Status = schema.NotificationStatusNormal
info.CreatedAt = now
info.UpdatedAt = now
info.ObjectID = req.ObjectInfo.ObjectID
userBasicInfo, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, req.TriggerUserID)
if err != nil {
return fmt.Errorf("get user basic info error: %w", err)
}
if !exist {
return fmt.Errorf("user not exist: %s", req.TriggerUserID)
}
req.UserInfo = userBasicInfo
content, _ := json.Marshal(req)
_, ok := constant.NotificationMsgTypeMapping[req.NotificationAction]
if ok {
info.MsgType = constant.NotificationMsgTypeMapping[req.NotificationAction]
}
info.Content = string(content)
err = ns.notificationRepo.AddNotification(ctx, info)
if err != nil {
return fmt.Errorf("add notification error: %w", err)
}
err = ns.addRedDot(ctx, info.UserID, msg.Type)
if err != nil {
log.Error("addRedDot Error", err.Error())
}
if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType {
err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"])
}
go ns.SendNotificationToAllFollower(ctx, msg, questionID)
if msg.Type == schema.NotificationTypeInbox {
ns.syncNotificationToPlugin(ctx, objInfo, msg)
}
return nil
}
func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error {
var key string
if noticeType == schema.NotificationTypeInbox {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
} else {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
}
_, exist, err := ns.data.Cache.GetInt64(ctx, key)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
if exist {
if _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return nil
}
err = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return nil
}
func (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error {
var key string
if notificationType == schema.NotificationTypeInbox {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
} else {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
}
_, exist, err := ns.data.Cache.GetInt64(ctx, key)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
if !exist {
return nil
}
res, err := ns.data.Cache.Decrease(ctx, key, 1)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
if res <= 0 {
return ns.DeleteRedDot(ctx, userID, notificationType)
}
return nil
}
func (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error {
var key string
if notificationType == schema.NotificationTypeInbox {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
} else {
key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
}
err := ns.data.Cache.Del(ctx, key)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return nil
}
// AddBadgeAwardAlertCache add badge award alert cache
func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) {
key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)
cacheData, exist, err := ns.data.Cache.GetString(ctx, key)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
if !exist {
c := schema.NewRedDotBadgeAwardCache()
c.AddBadgeAward(&schema.RedDotBadgeAward{
NotificationID: notificationID,
BadgeID: badgeID,
})
return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
}
c := schema.NewRedDotBadgeAwardCache()
c.FromJSON(cacheData)
c.AddBadgeAward(&schema.RedDotBadgeAward{
NotificationID: notificationID,
BadgeID: badgeID,
})
return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
}
// RemoveBadgeAwardAlertCache remove badge award alert cache
func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) {
key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)
cacheData, exist, err := ns.data.Cache.GetString(ctx, key)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
if !exist {
return nil
}
c := schema.NewRedDotBadgeAwardCache()
c.FromJSON(cacheData)
c.RemoveBadgeAward(notificationID)
if len(c.BadgeAwardList) == 0 {
return ns.data.Cache.Del(ctx, key)
}
return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
}
// SendNotificationToAllFollower send notification to all followers
func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg,
questionID string) {
if msg.NoNeedPushAllFollow || len(questionID) == 0 {
return
}
if msg.NotificationAction != constant.NotificationUpdateQuestion &&
msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
msg.NotificationAction != constant.NotificationUpdateAnswer &&
msg.NotificationAction != constant.NotificationAcceptAnswer {
return
}
condObjectID := msg.ObjectID
if len(questionID) > 0 {
condObjectID = uid.DeShortID(questionID)
}
userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID)
if err != nil {
log.Error(err)
return
}
log.Infof("send notification to all followers: %s %d", condObjectID, len(userIDs))
for _, userID := range userIDs {
t := &schema.NotificationMsg{}
_ = copier.Copy(t, msg)
t.ReceiverUserID = userID
t.TriggerUserID = msg.TriggerUserID
t.NoNeedPushAllFollow = true
ns.notificationQueueService.Send(ctx, t)
}
}
func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo,
msg *schema.NotificationMsg) {
if objInfo == nil {
return
}
siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site general info failed: %v", err)
return
}
seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Errorf("get site seo info failed: %v", err)
return
}
interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx)
if err != nil {
log.Errorf("get site interface info failed: %v", err)
return
}
objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
pluginNotificationMsg := plugin.NotificationMessage{
Type: plugin.NotificationType(msg.NotificationAction),
ReceiverUserID: msg.ReceiverUserID,
TriggerUserID: msg.TriggerUserID,
QuestionTitle: objInfo.Title,
}
if len(objInfo.QuestionID) > 0 {
pluginNotificationMsg.QuestionUrl =
display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title)
}
if len(objInfo.AnswerID) > 0 {
pluginNotificationMsg.AnswerUrl =
display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID)
}
if len(objInfo.CommentID) > 0 {
pluginNotificationMsg.CommentUrl =
display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID)
}
if len(msg.TriggerUserID) > 0 {
triggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID)
if err != nil {
log.Errorf("get trigger user basic info failed: %v", err)
return
}
if exist {
pluginNotificationMsg.TriggerUserID = triggerUser.ID
pluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName
pluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)
}
}
if len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 {
userInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID)
if userInfo != nil {
pluginNotificationMsg.ReceiverLang = userInfo.Language
}
// If receiver not set language, use site default language.
if len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption {
pluginNotificationMsg.ReceiverLang = interfaceInfo.Language
}
}
_ = plugin.CallNotification(func(fn plugin.Notification) error {
userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID)
if err != nil {
log.Errorf("get user external login info failed: %v", err)
return nil
}
if exist {
pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID
}
fn.Notify(pluginNotificationMsg)
return nil
})
}