diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index 254da6f..75305e7 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -14,14 +14,9 @@ func RunMigration(source *gorm.DB) error { &models.Category{}, &models.Tag{}, &models.Moment{}, - &models.MomentLike{}, - &models.MomentDislike{}, &models.Article{}, - &models.ArticleLike{}, - &models.ArticleDislike{}, &models.Comment{}, - &models.CommentLike{}, - &models.CommentDislike{}, + &models.Reaction{}, &models.Attachment{}, ); err != nil { return err diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index a2283b0..1abcd7a 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -8,24 +8,19 @@ import "time" type Account struct { BaseModel - Name string `json:"name"` - Nick string `json:"nick"` - Avatar string `json:"avatar"` - Description string `json:"description"` - EmailAddress string `json:"email_address"` - PowerLevel int `json:"power_level"` - Moments []Moment `json:"moments" gorm:"foreignKey:AuthorID"` - Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"` - Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` - LikedMoments []MomentLike `json:"liked_moments"` - DislikedMoments []MomentDislike `json:"disliked_moments"` - LikedArticles []ArticleLike `json:"liked_articles"` - DislikedArticles []ArticleDislike `json:"disliked_articles"` - LikedComments []CommentLike `json:"liked_comments"` - DislikedComments []CommentDislike `json:"disliked_comments"` - RealmIdentities []RealmMember `json:"identities"` - Realms []Realm `json:"realms"` - ExternalID uint `json:"external_id"` + Name string `json:"name"` + Nick string `json:"nick"` + Avatar string `json:"avatar"` + Description string `json:"description"` + EmailAddress string `json:"email_address"` + PowerLevel int `json:"power_level"` + Moments []Moment `json:"moments" gorm:"foreignKey:AuthorID"` + Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"` + Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` + Reactions []Reaction `json:"reactions"` + RealmIdentities []RealmMember `json:"identities"` + Realms []Realm `json:"realms"` + ExternalID uint `json:"external_id"` } type AccountMembership struct { diff --git a/pkg/models/articles.go b/pkg/models/articles.go index ed0a414..138cae6 100644 --- a/pkg/models/articles.go +++ b/pkg/models/articles.go @@ -3,15 +3,14 @@ package models type Article struct { PostBase - Title string `json:"title"` - Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"` - Categories []Category `json:"categories" gorm:"many2many:article_categories"` - LikedAccounts []ArticleLike `json:"liked_accounts"` - DislikedAccounts []ArticleDislike `json:"disliked_accounts"` - Description string `json:"description"` - Content string `json:"content"` - RealmID *uint `json:"realm_id"` - Realm *Realm `json:"realm"` + Title string `json:"title"` + Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"` + Categories []Category `json:"categories" gorm:"many2many:article_categories"` + Reactions []Reaction `json:"reactions"` + Description string `json:"description"` + Content string `json:"content"` + RealmID *uint `json:"realm_id"` + Realm *Realm `json:"realm"` Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"` } diff --git a/pkg/models/comments.go b/pkg/models/comments.go index 8393786..6d8387e 100644 --- a/pkg/models/comments.go +++ b/pkg/models/comments.go @@ -3,13 +3,12 @@ package models type Comment struct { PostBase - Content string `json:"content"` - Hashtags []Tag `json:"tags" gorm:"many2many:comment_tags"` - Categories []Category `json:"categories" gorm:"many2many:comment_categories"` - LikedAccounts []CommentLike `json:"liked_accounts"` - DislikedAccounts []CommentDislike `json:"disliked_accounts"` - ReplyID *uint `json:"reply_id"` - ReplyTo *Comment `json:"reply_to" gorm:"foreignKey:ReplyID"` + Content string `json:"content"` + Hashtags []Tag `json:"tags" gorm:"many2many:comment_tags"` + Categories []Category `json:"categories" gorm:"many2many:comment_categories"` + Reactions []Reaction `json:"reactions"` + ReplyID *uint `json:"reply_id"` + ReplyTo *Comment `json:"reply_to" gorm:"foreignKey:ReplyID"` ArticleID *uint `json:"article_id"` MomentID *uint `json:"moment_id"` diff --git a/pkg/models/moments.go b/pkg/models/moments.go index 9312b94..c6affb3 100644 --- a/pkg/models/moments.go +++ b/pkg/models/moments.go @@ -3,15 +3,14 @@ package models type Moment struct { PostBase - Content string `json:"content"` - Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"` - Categories []Category `json:"categories" gorm:"many2many:moment_categories"` - LikedAccounts []MomentLike `json:"liked_accounts"` - DislikedAccounts []MomentDislike `json:"disliked_accounts"` - RealmID *uint `json:"realm_id"` - RepostID *uint `json:"repost_id"` - Realm *Realm `json:"realm"` - RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"` + Content string `json:"content"` + Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"` + Categories []Category `json:"categories" gorm:"many2many:moment_categories"` + Reactions []Reaction `json:"reactions"` + RealmID *uint `json:"realm_id"` + RepostID *uint `json:"repost_id"` + Realm *Realm `json:"realm"` + RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"` Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"` } diff --git a/pkg/models/posts.go b/pkg/models/posts.go index 38c7752..b326e31 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -22,11 +22,7 @@ type PostBase struct { AuthorID uint `json:"author_id"` Author Account `json:"author"` - // Dynamic Calculating Values - LikeCount int64 `json:"like_count" gorm:"-"` - DislikeCount int64 `json:"dislike_count" gorm:"-"` - ReplyCount int64 `json:"reply_count" gorm:"-"` - RepostCount int64 `json:"repost_count" gorm:"-"` + // TODO Give the reactions & replies & reposts info back } func (p PostBase) GetID() uint { @@ -49,13 +45,6 @@ func (p PostBase) GetRealm() *Realm { return nil } -func (p PostBase) SetReactInfo(info PostReactInfo) { - p.LikeCount = info.LikeCount - p.DislikeCount = info.DislikeCount - p.ReplyCount = info.ReplyCount - p.RepostCount = info.RepostCount -} - type PostInterface interface { GetID() uint GetHashtags() []Tag @@ -67,5 +56,4 @@ type PostInterface interface { SetHashtags([]Tag) SetCategories([]Category) - SetReactInfo(PostReactInfo) } diff --git a/pkg/models/reactions.go b/pkg/models/reactions.go index d1a5385..0ffd22d 100644 --- a/pkg/models/reactions.go +++ b/pkg/models/reactions.go @@ -1,63 +1,27 @@ package models -import "time" +import ( + "time" +) -type CommentLike struct { +type ReactionAttitude = uint8 + +const ( + AttitudeNeutral = ReactionAttitude(iota) + AttitudePositive + AttitudeNegative +) + +type Reaction struct { ID uint `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` -} -type CommentDislike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` -} + Symbol string `json:"symbol"` + Attitude ReactionAttitude `json:"attitude"` -type MomentLike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` -} - -type MomentDislike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` -} - -type ArticleLike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` -} - -type ArticleDislike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` - AccountID uint `json:"account_id"` + ArticleID *uint `json:"article_id"` + MomentID *uint `json:"moment_id"` + CommentID *uint `json:"comment_id"` + AccountID uint `json:"account_id"` } diff --git a/pkg/server/moments_api.go b/pkg/server/moments_api.go index a25979a..876454f 100644 --- a/pkg/server/moments_api.go +++ b/pkg/server/moments_api.go @@ -164,7 +164,7 @@ func editMoment(c *fiber.Ctx) error { mx := getMomentContext().FilterAuthor(user.ID) - item, err := mx.Get(uint(id), true) + item, err := mx.Get(uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -188,29 +188,35 @@ func reactMoment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) id, _ := c.ParamsInt("momentId", 0) + var data struct { + Symbol string `json:"symbol" validate:"required"` + Attitude models.ReactionAttitude `json:"attitude" validate:"required"` + } + + if err := BindAndValidate(c, &data); err != nil { + return err + } + mx := getMomentContext() - item, err := mx.Get(uint(id), true) + item, err := mx.Get(uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - switch strings.ToLower(c.Params("reactType")) { - case "like": - if positive, err := mx.ReactLike(user, item.ID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) - } - case "dislike": - if positive, err := mx.ReactDislike(user, item.ID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) - } - default: - return fiber.NewError(fiber.StatusBadRequest, "unsupported reaction") + reaction := models.Reaction{ + Symbol: data.Symbol, + Attitude: data.Attitude, + AccountID: user.ID, + MomentID: &item.ID, } + + if positive, reaction, err := mx.React(reaction); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction) + } + } func deleteMoment(c *fiber.Ctx) error { @@ -219,7 +225,7 @@ func deleteMoment(c *fiber.Ctx) error { mx := getMomentContext().FilterAuthor(user.ID) - item, err := mx.Get(uint(id), true) + item, err := mx.Get(uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 9087efc..a1cb1fc 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -76,7 +76,7 @@ func NewServer() { moments.Get("/", listMoment) moments.Get("/:momentId", getMoment) moments.Post("/", authMiddleware, createMoment) - moments.Post("/:momentId/react/:reactType", authMiddleware, reactMoment) + moments.Post("/:momentId/react", authMiddleware, reactMoment) moments.Put("/:momentId", authMiddleware, editMoment) moments.Delete("/:momentId", authMiddleware, deleteMoment) } diff --git a/pkg/services/auth.go b/pkg/services/auth.go index ee3f64a..9ce0556 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -7,12 +7,16 @@ import ( "code.smartsheep.studio/hydrogen/interactive/pkg/models" "context" "errors" + "fmt" "gorm.io/gorm" "time" ) func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) { var account models.Account + if userinfo == nil { + return account, fmt.Errorf("remote userinfo was not found") + } if err := database.C.Where(&models.Account{ ExternalID: uint(userinfo.Id), }).First(&account).Error; err != nil { diff --git a/pkg/services/posts.go b/pkg/services/posts.go index abd8dc4..868a3ef 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -4,6 +4,7 @@ import ( "code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto" "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "errors" "fmt" pluralize "github.com/gertd/go-pluralize" "github.com/rs/zerolog/log" @@ -14,35 +15,6 @@ import ( "time" ) -const ( - reactUnionSelect = `SELECT t.id AS post_id, - COALESCE(l.like_count, 0) AS like_count, - COALESCE(d.dislike_count, 0) AS dislike_count--!COMMA!-- - --!REPLY_UNION_COLUMN!-- --!BOTH_COMMA!-- - --!REPOST_UNION_COLUMN!-- - FROM %s t - LEFT JOIN (SELECT %s_id, COUNT(*) AS like_count - FROM %s_likes - GROUP BY %s_id) l ON t.id = l.%s_id - LEFT JOIN (SELECT %s_id, COUNT(*) AS dislike_count - FROM %s_likes - GROUP BY %s_id) d ON t.id = d.%s_id - --!REPLY_UNION_SELECT!-- - --!REPOST_UNION_SELECT!-- - WHERE t.id = ?` - // TODO Solve for the cross table query(like articles -> comments) - replyUnionColumn = `COALESCE(r.reply_count, 0) AS reply_count` - replyUnionSelect = `LEFT JOIN (SELECT reply_id, COUNT(*) AS reply_count - FROM %s - WHERE reply_id IS NOT NULL - GROUP BY reply_id) r ON t.id = r.reply_id` - repostUnionColumn = `COALESCE(rp.repost_count, 0) AS repost_count` - repostUnionSelect = `LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count - FROM %s - WHERE repost_id IS NOT NULL - GROUP BY repost_id) rp ON t.id = rp.repost_id` -) - type PostTypeContext[T models.PostInterface] struct { Tx *gorm.DB @@ -62,7 +34,11 @@ func (v *PostTypeContext[T]) GetTableName(plural ...bool) string { } func (v *PostTypeContext[T]) Preload() *PostTypeContext[T] { - v.Tx.Preload("Author").Preload("Attachments").Preload("Categories").Preload("Hashtags") + v.Tx.Preload("Author"). + Preload("Attachments"). + Preload("Categories"). + Preload("Hashtags"). + Preload("Reactions") if v.CanReply { v.Tx.Preload("ReplyTo") @@ -123,45 +99,12 @@ func (v *PostTypeContext[T]) SortCreatedAt(order string) *PostTypeContext[T] { return v } -func (v *PostTypeContext[T]) BuildReactInfoSql() string { - column := strings.ToLower(v.TypeName) - table := viper.GetString("database.prefix") + v.GetTableName() - pluralTable := viper.GetString("database.prefix") + v.GetTableName(true) - sql := fmt.Sprintf(reactUnionSelect, pluralTable, column, table, column, column, column, table, column, column) - - if v.CanReply { - sql = strings.Replace(sql, "--!REPLY_UNION_COLUMN!--", replyUnionColumn, 1) - sql = strings.Replace(sql, "--!REPLY_UNION_SELECT!--", fmt.Sprintf(replyUnionSelect, pluralTable), 1) - } - if v.CanRepost { - sql = strings.Replace(sql, "--!REPOST_UNION_COLUMN!--", repostUnionColumn, 1) - sql = strings.Replace(sql, "--!REPOST_UNION_SELECT!--", fmt.Sprintf(repostUnionSelect, pluralTable), 1) - } - if v.CanReply || v.CanRepost { - sql = strings.ReplaceAll(sql, "--!COMMA!--", ",") - } - if v.CanReply && v.CanRepost { - sql = strings.ReplaceAll(sql, "--!BOTH_COMMA!--", ",") - } - - return sql -} - -func (v *PostTypeContext[T]) Get(id uint, noReact ...bool) (T, error) { +func (v *PostTypeContext[T]) Get(id uint) (T, error) { var item T if err := v.Preload().Tx.Where("id = ?", id).First(&item).Error; err != nil { return item, err } - var reactInfo models.PostReactInfo - - if len(noReact) <= 0 || !noReact[0] { - sql := v.BuildReactInfoSql() - database.C.Raw(sql, item.GetID()).Scan(&reactInfo) - } - - item.SetReactInfo(reactInfo) - return item, nil } @@ -185,33 +128,6 @@ func (v *PostTypeContext[T]) List(take int, offset int, noReact ...bool) ([]T, e return items, err } - idx := lo.Map(items, func(item T, _ int) uint { - return item.GetID() - }) - - var reactInfo []struct { - PostID uint `json:"post_id"` - LikeCount int64 `json:"like_count"` - DislikeCount int64 `json:"dislike_count"` - ReplyCount int64 `json:"reply_count"` - RepostCount int64 `json:"repost_count"` - } - - if len(noReact) <= 0 || !noReact[0] { - sql := v.BuildReactInfoSql() - database.C.Raw(sql, idx).Scan(&reactInfo) - } - - itemMap := lo.SliceToMap(items, func(item T) (uint, T) { - return item.GetID(), item - }) - - for _, info := range reactInfo { - if item, ok := itemMap[info.PostID]; ok { - item.SetReactInfo(info) - } - } - return items, nil } @@ -320,32 +236,14 @@ func (v *PostTypeContext[T]) Delete(item T) error { return database.C.Delete(&item).Error } -func (v *PostTypeContext[T]) ReactLike(user models.Account, id uint) (bool, error) { - var count int64 - table := viper.GetString("database.prefix") + v.GetTableName() + "_likes" - tx := database.C.Where("account_id = ?", user.ID).Where(v.GetTableName()+"id = ?", id) - if tx.Count(&count); count <= 0 { - return true, database.C.Table(table).Create(map[string]any{ - "AccountID": user.ID, - v.TypeName + "ID": id, - }).Error +func (v *PostTypeContext[T]) React(reaction models.Reaction) (bool, models.Reaction, error) { + if err := database.C.Where(reaction).First(&reaction).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, reaction, database.C.Save(&reaction).Error + } else { + return true, reaction, err + } } else { - column := strings.ToLower(v.TypeName) - return false, tx.Raw(fmt.Sprintf("DELETE FROM %s WHERE account_id = ? AND %s_id = ?", table, column), user.ID, id).Error - } -} - -func (v *PostTypeContext[T]) ReactDislike(user models.Account, id uint) (bool, error) { - var count int64 - table := viper.GetString("database.prefix") + v.GetTableName() + "_dislikes" - tx := database.C.Where("account_id = ?", user.ID).Where(v.GetTableName()+"id = ?", id) - if tx.Count(&count); count <= 0 { - return true, database.C.Table(table).Create(map[string]any{ - "AccountID": user.ID, - v.TypeName + "ID": id, - }).Error - } else { - column := strings.ToLower(v.TypeName) - return false, tx.Raw(fmt.Sprintf("DELETE FROM %s WHERE account_id = ? AND %s_id = ?", table, column), user.ID, id).Error + return false, reaction, database.C.Delete(&reaction).Error } }