diff --git a/pkg/internal/database/migrator.go b/pkg/internal/database/migrator.go index c5b22b4..a3545e9 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -26,6 +26,7 @@ var AutoMaintainRange = []any{ &models.AuditRecord{}, &models.ApiKey{}, &models.SignRecord{}, + &models.PreferenceNotification{}, } func RunMigration(source *gorm.DB) error { diff --git a/pkg/internal/models/preferences.go b/pkg/internal/models/preferences.go new file mode 100644 index 0000000..9e87e09 --- /dev/null +++ b/pkg/internal/models/preferences.go @@ -0,0 +1,11 @@ +package models + +import "gorm.io/datatypes" + +type PreferenceNotification struct { + BaseModel + + Config datatypes.JSONMap `json:"config"` + AccountID uint `json:"account_id"` + Account Account `json:"account"` +} diff --git a/pkg/internal/server/admin/notify_api.go b/pkg/internal/server/admin/notify_api.go index 0c2794d..1d71c06 100644 --- a/pkg/internal/server/admin/notify_api.go +++ b/pkg/internal/server/admin/notify_api.go @@ -52,6 +52,7 @@ func notifyAllUser(c *fiber.Ctx) error { Picture: data.Picture, IsRealtime: data.IsRealtime, IsForcePush: data.IsForcePush, + Account: user, AccountID: user.ID, } diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index f053b6c..53c61ab 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -24,6 +24,12 @@ func MapAPIs(app *fiber.App, baseURL string) { notify.Put("/read/:notificationId", markNotificationRead) } + preferences := api.Group("/preferences").Name("Preferences API") + { + preferences.Get("/notifications", getNotificationPreference) + preferences.Put("/notifications", updateNotificationPreference) + } + api.Get("/users/lookup", lookupAccount) api.Get("/users/search", searchAccount) diff --git a/pkg/internal/server/api/notify_api.go b/pkg/internal/server/api/notify_api.go index c6ad390..779eca5 100644 --- a/pkg/internal/server/api/notify_api.go +++ b/pkg/internal/server/api/notify_api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "git.solsynth.dev/hydrogen/passport/pkg/internal/models" "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" "git.solsynth.dev/hydrogen/passport/pkg/internal/services" @@ -52,6 +53,7 @@ func notifyUser(c *fiber.Ctx) error { Picture: data.Picture, IsRealtime: data.IsRealtime, IsForcePush: data.IsForcePush, + Account: target, AccountID: target.ID, SenderID: &client.ID, } diff --git a/pkg/internal/server/api/preferences_api.go b/pkg/internal/server/api/preferences_api.go new file mode 100644 index 0000000..a821952 --- /dev/null +++ b/pkg/internal/server/api/preferences_api.go @@ -0,0 +1,43 @@ +package api + +import ( + "git.solsynth.dev/hydrogen/passport/pkg/internal/models" + "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/passport/pkg/internal/services" + "github.com/gofiber/fiber/v2" +) + +func getNotificationPreference(c *fiber.Ctx) error { + if err := exts.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + notification, err := services.GetNotificationPreference(user) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + return c.JSON(notification) +} + +func updateNotificationPreference(c *fiber.Ctx) error { + if err := exts.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Config map[string]bool `json:"config"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + notification, err := services.UpdateNotificationPreference(user, data.Config) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(notification) +} diff --git a/pkg/internal/services/notifications.go b/pkg/internal/services/notifications.go index 0ea6279..158d5dc 100644 --- a/pkg/internal/services/notifications.go +++ b/pkg/internal/services/notifications.go @@ -3,12 +3,14 @@ package services import ( "context" "fmt" - "git.solsynth.dev/hydrogen/dealer/pkg/hyper" - jsoniter "github.com/json-iterator/go" - "github.com/samber/lo" "reflect" "time" + "git.solsynth.dev/hydrogen/dealer/pkg/hyper" + jsoniter "github.com/json-iterator/go" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "git.solsynth.dev/hydrogen/dealer/pkg/proto" "git.solsynth.dev/hydrogen/passport/pkg/internal/gap" @@ -47,7 +49,13 @@ func AddNotifySubscriber(user models.Account, provider, id, tk, ua string) (mode } // NewNotification will create a notification and push via the push method it +// Please provide the notification with the account field is not empty func NewNotification(notification models.Notification) error { + if ok := CheckNotificationNotifiable(notification.Account, notification.Topic); !ok { + log.Info().Str("topic", notification.Topic).Uint("uid", notification.AccountID).Msg("Notification dismissed by user...") + return nil + } + if err := database.C.Save(¬ification).Error; err != nil { return err } @@ -67,7 +75,14 @@ func NewNotificationBatch(notifications []models.Notification) error { return nil } +// PushNotification will push a notification to the user, via websocket, firebase, or APNs +// Please provide the notification with the account field is not empty func PushNotification(notification models.Notification) error { + if ok := CheckNotificationNotifiable(notification.Account, notification.Topic); !ok { + log.Info().Str("topic", notification.Topic).Uint("uid", notification.AccountID).Msg("Notification dismissed by user...") + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := proto.NewStreamControllerClient(gap.H.GetDealerGrpcConn()).PushStream(ctx, &proto.PushStreamRequest{ @@ -124,9 +139,26 @@ func PushNotification(notification models.Notification) error { } func PushNotificationBatch(notifications []models.Notification) { - accountIdx := lo.Map(notifications, func(item models.Notification, index int) uint { - return item.AccountID - }) + if len(notifications) == 0 { + return + } + + notifiable := CheckNotificationNotifiableBatch(lo.Map(notifications, func(item models.Notification, index int) models.Account { + return item.Account + }), notifications[0].Topic) + accountIdx := lo.Map( + lo.Filter(notifications, func(item models.Notification, index int) bool { + return notifiable[index] + }), + func(item models.Notification, index int) uint { + return item.AccountID + }, + ) + + if len(accountIdx) == 0 { + return + } + var subscribers []models.NotificationSubscriber database.C.Where("account_id IN ?", accountIdx).Find(&subscribers) diff --git a/pkg/internal/services/preferences.go b/pkg/internal/services/preferences.go new file mode 100644 index 0000000..bd76c3e --- /dev/null +++ b/pkg/internal/services/preferences.go @@ -0,0 +1,78 @@ +package services + +import ( + "errors" + + "git.solsynth.dev/hydrogen/passport/pkg/internal/database" + "git.solsynth.dev/hydrogen/passport/pkg/internal/models" + "github.com/samber/lo" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +func GetNotificationPreference(account models.Account) (models.PreferenceNotification, error) { + var notification models.PreferenceNotification + if err := database.C.Where("account_id = ?", account.ID).First(¬ification).Error; err != nil { + return notification, err + } + return notification, nil +} + +func UpdateNotificationPreference(account models.Account, config map[string]bool) (models.PreferenceNotification, error) { + var notification models.PreferenceNotification + var err error + if notification, err = GetNotificationPreference(account); err != nil { + notification = models.PreferenceNotification{ + AccountID: account.ID, + Config: datatypes.JSONMap( + lo.MapValues(config, func(v bool, k string) any { return v }), + ), + } + } + + err = database.C.Save(¬ification).Error + return notification, err +} + +func CheckNotificationNotifiable(account models.Account, topic string) bool { + var notification models.PreferenceNotification + if err := database.C.Where("account_id = ?", account.ID).First(¬ification).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true + } + return false + } + if val, ok := notification.Config[topic]; ok { + if status, ok := val.(bool); !ok || status { + return true + } else if !status { + return false + } + } + return true +} + +func CheckNotificationNotifiableBatch(accounts []models.Account, topic string) []bool { + var notifications []models.PreferenceNotification + if err := database.C.Where("account_id IN ?", accounts).Find(¬ifications).Error; err != nil { + return lo.Map(accounts, func(item models.Account, index int) bool { + return false + }) + } + + var notifiable []bool + for _, notification := range notifications { + if val, ok := notification.Config[topic]; ok { + if status, ok := val.(bool); !ok || status { + notifiable = append(notifiable, true) + continue + } else if !status { + notifiable = append(notifiable, false) + continue + } + } + notifiable = append(notifiable, true) + } + + return notifiable +} diff --git a/pkg/internal/services/relationships.go b/pkg/internal/services/relationships.go index e18b189..731863d 100644 --- a/pkg/internal/services/relationships.go +++ b/pkg/internal/services/relationships.go @@ -113,6 +113,7 @@ func NewFriend(userA models.Account, userB models.Account, skipPending ...bool) Title: "New Friend Request", Subtitle: lo.ToPtr(fmt.Sprintf("New friend request from %s", userA.Name)), Body: fmt.Sprintf("You got a new friend request from %s. Go to your account page and decide how to deal it.", userA.Nick), + Account: userB, AccountID: userB.ID, }) } @@ -149,6 +150,7 @@ func HandleFriend(userA models.Account, userB models.Account, isAccept bool) err Title: "Friend Request Processed", Subtitle: lo.ToPtr(fmt.Sprintf("Your friend request to %s has been processsed.", userA.Name)), Body: fmt.Sprintf("Your relationship status with %s has been updated, go check it out!", userA.Nick), + Account: userB, AccountID: userB.ID, }) }