/* * 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/noticequeue" "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 noticequeue.Service 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 noticequeue.Service, 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"]) if err != nil { log.Error("AddBadgeAwardAlertCache Error", err.Error()) } } 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 } } externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, msg.ReceiverUserID) if err != nil { log.Errorf("get user external login list failed for user %s: %v", msg.ReceiverUserID, err) } else if len(externalLogins) > 0 { pluginNotificationMsg.ReceiverExternalID = externalLogins[0].ExternalID } _ = 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 }) }