/* * 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 notification import ( "context" "encoding/json" "fmt" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/badge" notficationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/report_common" "github.com/apache/answer/internal/service/review" "github.com/apache/answer/internal/service/revision_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/log" ) // NotificationService user service type NotificationService struct { data *data.Data notificationRepo notficationcommon.NotificationRepo notificationCommon *notficationcommon.NotificationCommon revisionService *revision_common.RevisionService reportRepo report_common.ReportRepo reviewService *review.ReviewService userRepo usercommon.UserRepo badgeRepo badge.BadgeRepo } func NewNotificationService( data *data.Data, notificationRepo notficationcommon.NotificationRepo, notificationCommon *notficationcommon.NotificationCommon, revisionService *revision_common.RevisionService, userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, badgeRepo badge.BadgeRepo, ) *NotificationService { return &NotificationService{ data: data, notificationRepo: notificationRepo, notificationCommon: notificationCommon, revisionService: revisionService, userRepo: userRepo, reportRepo: reportRepo, reviewService: reviewService, badgeRepo: badgeRepo, } } func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { inboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID) achievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID) redBot := &schema.RedDot{} redBot.Inbox, _, _ = ns.data.Cache.GetInt64(ctx, inboxKey) redBot.Achievement, _, _ = ns.data.Cache.GetInt64(ctx, achievementKey) // get review amount if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { redBot.CanRevision = true redBot.Revision = ns.countAllReviewAmount(ctx, req) } // get badge award redBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID) return redBot, nil } func (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) { key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) cacheData, exist, err := ns.data.Cache.GetString(ctx, key) if err != nil { log.Errorf("get badge award failed: %v", err) return nil } if !exist { return nil } c := schema.NewRedDotBadgeAwardCache() c.FromJSON(cacheData) award := c.GetBadgeAward() if award == nil { return nil } badgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID) if err != nil { log.Errorf("get badge info failed: %v", err) return nil } if !exists { return nil } award.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name) award.Icon = badgeInfo.Icon award.Level = badgeInfo.Level return award } func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { // get queue amount if req.IsAdmin { reviewCount, err := ns.reviewService.GetReviewPendingCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { amount += reviewCount } } // get flag amount if req.IsAdmin { reportCount, err := ns.reportRepo.GetReportCount(ctx) if err != nil { log.Errorf("get report count failed: %v", err) } else { amount += reportCount } } // get suggestion amount countUnreviewedRevision, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, &schema.RevisionSearch{ CanReviewQuestion: req.CanReviewQuestion, CanReviewAnswer: req.CanReviewAnswer, CanReviewTag: req.CanReviewTag, UserID: req.UserID, }) if err != nil { log.Errorf("get unreviewed revision count failed: %v", err) } else { amount += countUnreviewedRevision } return amount } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { _ = ns.notificationCommon.DeleteRedDot(ctx, req.UserID, schema.NotificationType[req.NotificationType]) resp := &schema.GetRedDot{} _ = copier.Copy(resp, req) return ns.GetRedDot(ctx, resp) } func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error { botType, ok := schema.NotificationType[notificationType] if ok { err := ns.notificationRepo.ClearUnRead(ctx, userID, botType) if err != nil { return err } } return nil } func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error { notificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id) if err != nil { log.Errorf("get notification failed: %v", err) return nil } if !exist || notificationInfo.UserID != userID { return nil } if notificationInfo.IsRead == schema.NotificationNotRead { err := ns.notificationRepo.ClearIDUnRead(ctx, userID, id) if err != nil { return err } } err = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id) if err != nil { log.Errorf("remove badge award alert cache failed: %v", err) } _ = ns.notificationCommon.DecreaseRedDot(ctx, userID, notificationInfo.Type) return nil } func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( pageModel *pager.PageModel, err error) { resp := make([]*schema.NotificationContent, 0) searchType, ok := schema.NotificationType[searchCond.TypeStr] if !ok { return pager.NewPageModel(0, resp), nil } searchInboxType := schema.NotificationInboxTypeAll if searchType == schema.NotificationTypeInbox { _, ok = schema.NotificationInboxType[searchCond.InboxTypeStr] if ok { searchInboxType = schema.NotificationInboxType[searchCond.InboxTypeStr] } } searchCond.Type = searchType searchCond.InboxType = searchInboxType notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) if err != nil { return nil, err } resp = ns.formatNotificationPage(ctx, notifications) return pager.NewPageModel(total, resp), nil } func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) ( resp []*schema.NotificationContent) { lang := handler.GetLangByCtx(ctx) enableShortID := handler.GetEnableShortID(ctx) userIDs := make([]string, 0) userMapping := make(map[string]bool) for _, notificationInfo := range notifications { item := &schema.NotificationContent{} if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil { log.Error("NotificationContent Unmarshal Error", err.Error()) continue } // If notification is downvote, the user info is not needed. if item.NotificationAction == constant.NotificationDownVotedTheQuestion || item.NotificationAction == constant.NotificationDownVotedTheAnswer { item.UserInfo = nil } // If notification is badge, the user info is not needed and the title need to be translated. if item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { badgeName := translator.Tr(lang, item.ObjectInfo.Title) item.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct { BadgeName string }{BadgeName: badgeName}) item.UserInfo = nil } item.ID = notificationInfo.ID item.NotificationAction = translator.Tr(lang, item.NotificationAction) item.UpdateTime = notificationInfo.UpdatedAt.Unix() item.IsRead = notificationInfo.IsRead == schema.NotificationRead if enableShortID { if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { if item.ObjectInfo.ObjectID == answerID { item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) } item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) } if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { if item.ObjectInfo.ObjectID == questionID { item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) } } if item.UserInfo != nil && !userMapping[item.UserInfo.ID] { userIDs = append(userIDs, item.UserInfo.ID) userMapping[item.UserInfo.ID] = true } resp = append(resp, item) } if len(userIDs) == 0 { return resp } users, err := ns.userRepo.BatchGetByID(ctx, userIDs) if err != nil { log.Error(err) return resp } userIDMapping := make(map[string]*entity.User, len(users)) for _, user := range users { userIDMapping[user.ID] = user } for _, item := range resp { if item.UserInfo == nil { continue } userInfo, ok := userIDMapping[item.UserInfo.ID] if !ok { continue } if userInfo.Status == entity.UserStatusDeleted { item.UserInfo = &schema.UserBasicInfo{ DisplayName: "user" + converter.DeleteUserDisplay(userInfo.ID), Status: constant.UserDeleted, } } } return resp }