✨ Notification preferences
This commit is contained in:
		| @@ -26,6 +26,7 @@ var AutoMaintainRange = []any{ | ||||
| 	&models.AuditRecord{}, | ||||
| 	&models.ApiKey{}, | ||||
| 	&models.SignRecord{}, | ||||
| 	&models.PreferenceNotification{}, | ||||
| } | ||||
|  | ||||
| func RunMigration(source *gorm.DB) error { | ||||
|   | ||||
							
								
								
									
										11
									
								
								pkg/internal/models/preferences.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								pkg/internal/models/preferences.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"` | ||||
| } | ||||
| @@ -52,6 +52,7 @@ func notifyAllUser(c *fiber.Ctx) error { | ||||
| 				Picture:     data.Picture, | ||||
| 				IsRealtime:  data.IsRealtime, | ||||
| 				IsForcePush: data.IsForcePush, | ||||
| 				Account:     user, | ||||
| 				AccountID:   user.ID, | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										43
									
								
								pkg/internal/server/api/preferences_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								pkg/internal/server/api/preferences_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										78
									
								
								pkg/internal/services/preferences.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								pkg/internal/services/preferences.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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, | ||||
| 		}) | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user