♻️ Interactive v2 #1
| @@ -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 | ||||
|   | ||||
| @@ -17,12 +17,7 @@ type Account struct { | ||||
| 	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"` | ||||
| 	Reactions       []Reaction    `json:"reactions"` | ||||
| 	RealmIdentities []RealmMember `json:"identities"` | ||||
| 	Realms          []Realm       `json:"realms"` | ||||
| 	ExternalID      uint          `json:"external_id"` | ||||
|   | ||||
| @@ -6,8 +6,7 @@ type Article struct { | ||||
| 	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"` | ||||
| 	Reactions   []Reaction `json:"reactions"` | ||||
| 	Description string     `json:"description"` | ||||
| 	Content     string     `json:"content"` | ||||
| 	RealmID     *uint      `json:"realm_id"` | ||||
|   | ||||
| @@ -6,8 +6,7 @@ type Comment struct { | ||||
| 	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"` | ||||
| 	Reactions  []Reaction `json:"reactions"` | ||||
| 	ReplyID    *uint      `json:"reply_id"` | ||||
| 	ReplyTo    *Comment   `json:"reply_to" gorm:"foreignKey:ReplyID"` | ||||
|  | ||||
|   | ||||
| @@ -6,8 +6,7 @@ type Moment struct { | ||||
| 	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"` | ||||
| 	Reactions  []Reaction `json:"reactions"` | ||||
| 	RealmID    *uint      `json:"realm_id"` | ||||
| 	RepostID   *uint      `json:"repost_id"` | ||||
| 	Realm      *Realm     `json:"realm"` | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -1,61 +1,25 @@ | ||||
| package models | ||||
|  | ||||
| import "time" | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type CommentLike 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"` | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| 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"` | ||||
|  | ||||
| 	Symbol   string           `json:"symbol"` | ||||
| 	Attitude ReactionAttitude `json:"attitude"` | ||||
|  | ||||
| 	ArticleID *uint `json:"article_id"` | ||||
| 	MomentID  *uint `json:"moment_id"` | ||||
| 	CommentID *uint `json:"comment_id"` | ||||
|   | ||||
| @@ -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 { | ||||
| 	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.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") | ||||
| 		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()) | ||||
| 	} | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
| 		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 true, reaction, err | ||||
| 		} | ||||
| } | ||||
|  | ||||
| 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 | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user