diff --git a/pkg/internal/database/migrator.go b/pkg/internal/database/migrator.go index 0b45570..9bc97d3 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -12,6 +12,7 @@ var AutoMaintainRange = []any{ &models.Tag{}, &models.Post{}, &models.Reaction{}, + &models.Subscription{}, } func RunMigration(source *gorm.DB) error { diff --git a/pkg/internal/models/accounts.go b/pkg/internal/models/accounts.go index c9b426b..7ec983c 100644 --- a/pkg/internal/models/accounts.go +++ b/pkg/internal/models/accounts.go @@ -5,8 +5,9 @@ import "git.solsynth.dev/hydrogen/dealer/pkg/hyper" type Account struct { hyper.BaseUser - Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` - Reactions []Reaction `json:"reactions"` + Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` + Reactions []Reaction `json:"reactions"` + Subscriptions []Subscription `json:"subscriptions" gorm:"foreginKey:FollowerID"` TotalUpvote int `json:"total_upvote"` TotalDownvote int `json:"total_downvote"` diff --git a/pkg/internal/models/subscriptions.go b/pkg/internal/models/subscriptions.go new file mode 100644 index 0000000..a904260 --- /dev/null +++ b/pkg/internal/models/subscriptions.go @@ -0,0 +1,16 @@ +package models + +import "git.solsynth.dev/hydrogen/dealer/pkg/hyper" + +type Subscription struct { + hyper.BaseModel + + FollowerID uint `json:"follower_id"` + Follower Account `json:"follower"` + AccountID *uint `json:"account_id,omitempty"` + Account *Account `json:"account,omitempty"` + TagID *uint `json:"tag_id,omitempty"` + Tag Tag `json:"tag,omitempty"` + CategoryID *uint `json:"category_id,omitempty"` + Category Category `json:"category,omitempty"` +} diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index f6ce7fb..178d3ba 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -45,6 +45,19 @@ func MapAPIs(app *fiber.App, baseURL string) { posts.Get("/:postId/replies/featured", listPostFeaturedReply) } + subscriptions := api.Group("/subscriptions").Name("Subscriptions API") + { + subscriptions.Get("/users/:userId", getSubscriptionOnUser) + subscriptions.Get("/tags/:tagId", getSubscriptionOnTag) + subscriptions.Get("/categories/:categoryId", getSubscriptionOnCategory) + subscriptions.Post("/users/:userId", subscribeToUser) + subscriptions.Post("/tags/:tagId", subscribeToTag) + subscriptions.Post("/categories/:categoryId", subscribeToCategory) + subscriptions.Delete("/users/:userId", unsubscribeFromUser) + subscriptions.Delete("/tags/:tagId", unsubscribeFromTag) + subscriptions.Delete("/categories/:categoryId", unsubscribeFromCategory) + } + api.Get("/categories", listCategories) api.Get("/categories/:category", getCategory) api.Post("/categories", newCategory) diff --git a/pkg/internal/server/api/subscriptions_api.go b/pkg/internal/server/api/subscriptions_api.go new file mode 100644 index 0000000..ade62ac --- /dev/null +++ b/pkg/internal/server/api/subscriptions_api.go @@ -0,0 +1,190 @@ +package api + +import ( + "fmt" + + "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" +) + +func getSubscriptionOnUser(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + otherUserId, err := c.ParamsInt("userId", 0) + otherUser, err := services.GetAccountWithID(uint(otherUserId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get user: %v", err)) + } + + subscription, err := services.GetSubscriptionOnUser(user, otherUser) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get subscription: %v", err)) + } + + return c.JSON(subscription) +} + +func getSubscriptionOnTag(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + tagId, err := c.ParamsInt("tagId", 0) + tag, err := services.GetTagWithID(uint(tagId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get tag: %v", err)) + } + + subscription, err := services.GetSubscriptionOnTag(user, tag) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get subscription: %v", err)) + } + + return c.JSON(subscription) +} + +func getSubscriptionOnCategory(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + categoryId, err := c.ParamsInt("categoryId", 0) + category, err := services.GetCategoryWithID(uint(categoryId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get category: %v", err)) + } + + subscription, err := services.GetSubscriptionOnCategory(user, category) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get subscription: %v", err)) + } + + return c.JSON(subscription) +} + +func subscribeToUser(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + otherUserId, err := c.ParamsInt("userId", 0) + otherUser, err := services.GetAccountWithID(uint(otherUserId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get user: %v", err)) + } + + subscription, err := services.SubscribeToUser(user, otherUser) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to subscribe to user: %v", err)) + } + + return c.JSON(subscription) +} + +func subscribeToTag(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + tagId, err := c.ParamsInt("tagId", 0) + tag, err := services.GetTagWithID(uint(tagId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get tag: %v", err)) + } + + subscription, err := services.SubscribeToTag(user, tag) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to subscribe to tag: %v", err)) + } + + return c.JSON(subscription) +} + +func subscribeToCategory(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + categoryId, err := c.ParamsInt("categoryId", 0) + category, err := services.GetCategoryWithID(uint(categoryId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get category: %v", err)) + } + + subscription, err := services.SubscribeToCategory(user, category) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to subscribe to category: %v", err)) + } + + return c.JSON(subscription) +} + +func unsubscribeFromUser(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + otherUserId, err := c.ParamsInt("userId", 0) + otherUser, err := services.GetAccountWithID(uint(otherUserId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get user: %v", err)) + } + + err = services.UnsubscribeFromUser(user, otherUser) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to unsubscribe from user: %v", err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func unsubscribeFromTag(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + tagId, err := c.ParamsInt("tagId", 0) + tag, err := services.GetTagWithID(uint(tagId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get tag: %v", err)) + } + + err = services.UnsubscribeFromTag(user, tag) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to unsubscribe from tag: %v", err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func unsubscribeFromCategory(c *fiber.Ctx) error { + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + categoryId, err := c.ParamsInt("categoryId", 0) + category, err := services.GetCategoryWithID(uint(categoryId)) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("unable to get category: %v", err)) + } + + err = services.UnsubscribeFromCategory(user, category) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to unsubscribe from category: %v", err)) + } + + return c.SendStatus(fiber.StatusOK) +} diff --git a/pkg/internal/services/accounts.go b/pkg/internal/services/accounts.go index 002ab51..877a6d5 100644 --- a/pkg/internal/services/accounts.go +++ b/pkg/internal/services/accounts.go @@ -3,6 +3,8 @@ package services import ( "context" "fmt" + "time" + "git.solsynth.dev/hydrogen/dealer/pkg/hyper" "git.solsynth.dev/hydrogen/dealer/pkg/proto" "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" @@ -10,9 +12,16 @@ import ( "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" "github.com/rs/zerolog/log" "github.com/samber/lo" - "time" ) +func GetAccountWithID(id uint) (models.Account, error) { + var account models.Account + if err := database.C.Where("id = ?", id).First(&account).Error; err != nil { + return account, fmt.Errorf("unable to get account by id: %v", err) + } + return account, nil +} + func ListAccountFriends(user models.Account) ([]models.Account, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() diff --git a/pkg/internal/services/categories.go b/pkg/internal/services/categories.go index e998979..4d7d29c 100644 --- a/pkg/internal/services/categories.go +++ b/pkg/internal/services/categories.go @@ -61,6 +61,16 @@ func DeleteCategory(category models.Category) error { return database.C.Delete(category).Error } +func GetTagWithID(id uint) (models.Tag, error) { + var tag models.Tag + if err := database.C.Where(models.Tag{ + BaseModel: hyper.BaseModel{ID: id}, + }).First(&tag).Error; err != nil { + return tag, err + } + return tag, nil +} + func GetTagOrCreate(alias, name string) (models.Tag, error) { alias = strings.ToLower(alias) var tag models.Tag diff --git a/pkg/internal/services/subscriptions.go b/pkg/internal/services/subscriptions.go new file mode 100644 index 0000000..77974c6 --- /dev/null +++ b/pkg/internal/services/subscriptions.go @@ -0,0 +1,136 @@ +package services + +import ( + "errors" + "fmt" + + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "gorm.io/gorm" +) + +func GetSubscriptionOnUser(user models.Account, target models.Account) (*models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND account_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("unable to get subscription: %v", err) + } + return &subscription, nil +} + +func GetSubscriptionOnTag(user models.Account, target models.Tag) (*models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND tag_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("unable to get subscription: %v", err) + } + return &subscription, nil +} + +func GetSubscriptionOnCategory(user models.Account, target models.Category) (*models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND category_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("unable to get subscription: %v", err) + } + return &subscription, nil +} + +func SubscribeToUser(user models.Account, target models.Account) (models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND account_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return subscription, fmt.Errorf("subscription already exists") + } + return subscription, fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + subscription = models.Subscription{ + FollowerID: user.ID, + AccountID: &target.ID, + } + + err := database.C.Save(&subscription).Error + return subscription, err +} + +func SubscribeToTag(user models.Account, target models.Tag) (models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND tag_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return subscription, fmt.Errorf("subscription already exists") + } + return subscription, fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + subscription = models.Subscription{ + FollowerID: user.ID, + TagID: &target.ID, + } + + err := database.C.Save(&subscription).Error + return subscription, err +} + +func SubscribeToCategory(user models.Account, target models.Category) (models.Subscription, error) { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND category_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return subscription, fmt.Errorf("subscription already exists") + } + return subscription, fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + subscription = models.Subscription{ + FollowerID: user.ID, + CategoryID: &target.ID, + } + + err := database.C.Save(&subscription).Error + return subscription, err +} + +func UnsubscribeFromUser(user models.Account, target models.Account) error { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND account_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("subscription does not exist") + } + return fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + err := database.C.Delete(&subscription).Error + return err +} + +func UnsubscribeFromTag(user models.Account, target models.Tag) error { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND tag_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("subscription does not exist") + } + return fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + err := database.C.Delete(&subscription).Error + return err +} + +func UnsubscribeFromCategory(user models.Account, target models.Category) error { + var subscription models.Subscription + if err := database.C.Where("follower_id = ? AND category_id = ?", user.ID, target.ID).First(&subscription).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("subscription does not exist") + } + return fmt.Errorf("unable to check subscription is exists or not: %v", err) + } + + err := database.C.Delete(&subscription).Error + return err +}