diff --git a/go.mod b/go.mod index 8469117..7ddc71e 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gertd/go-pluralize v0.2.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect diff --git a/go.sum b/go.sum index 0f13a7b..4b81518 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index b28fad1..254da6f 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -13,9 +13,15 @@ func RunMigration(source *gorm.DB) error { &models.RealmMember{}, &models.Category{}, &models.Tag{}, - &models.Post{}, - &models.PostLike{}, - &models.PostDislike{}, + &models.Moment{}, + &models.MomentLike{}, + &models.MomentDislike{}, + &models.Article{}, + &models.ArticleLike{}, + &models.ArticleDislike{}, + &models.Comment{}, + &models.CommentLike{}, + &models.CommentDislike{}, &models.Attachment{}, ); err != nil { return err diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index 3586de8..a2283b0 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -8,19 +8,24 @@ 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"` - Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` - Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` - LikedPosts []PostLike `json:"liked_posts"` - DislikedPosts []PostDislike `json:"disliked_posts"` - 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"` + 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"` } type AccountMembership struct { diff --git a/pkg/models/articles.go b/pkg/models/articles.go new file mode 100644 index 0000000..ed0a414 --- /dev/null +++ b/pkg/models/articles.go @@ -0,0 +1,41 @@ +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"` + + Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"` +} + +func (p Article) GetReplyTo() PostInterface { + return nil +} + +func (p Article) GetRepostTo() PostInterface { + return nil +} + +func (p Article) GetHashtags() []Tag { + return p.Hashtags +} + +func (p Article) GetCategories() []Category { + return p.Categories +} + +func (p Article) SetHashtags(tags []Tag) { + p.Hashtags = tags +} + +func (p Article) SetCategories(categories []Category) { + p.Categories = categories +} diff --git a/pkg/models/attachments.go b/pkg/models/attachments.go index da1de5c..0bbcbd5 100644 --- a/pkg/models/attachments.go +++ b/pkg/models/attachments.go @@ -6,18 +6,29 @@ import ( "path/filepath" ) +type AttachmentType = uint8 + +const ( + AttachmentOthers = AttachmentType(iota) + AttachmentPhoto + AttachmentVideo + AttachmentAudio +) + type Attachment struct { BaseModel - FileID string `json:"file_id"` - Filesize int64 `json:"filesize"` - Filename string `json:"filename"` - Mimetype string `json:"mimetype"` - ExternalUrl string `json:"external_url"` - Post *Post `json:"post"` - Author Account `json:"author"` - PostID *uint `json:"post_id"` - AuthorID uint `json:"author_id"` + FileID string `json:"file_id"` + Filesize int64 `json:"filesize"` + Filename string `json:"filename"` + Mimetype string `json:"mimetype"` + Type AttachmentType `json:"type"` + ExternalUrl string `json:"external_url"` + Author Account `json:"author"` + ArticleID *uint `json:"article_id"` + MomentID *uint `json:"moment_id"` + CommentID *uint `json:"comment_id"` + AuthorID uint `json:"author_id"` } func (v Attachment) GetStoragePath() string { diff --git a/pkg/models/categories.go b/pkg/models/categories.go index d3568fb..4048e4e 100644 --- a/pkg/models/categories.go +++ b/pkg/models/categories.go @@ -3,17 +3,21 @@ package models type Tag struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` - Name string `json:"name"` - Description string `json:"description"` - Posts []Post `json:"posts" gorm:"many2many:post_tags"` + Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` + Name string `json:"name"` + Description string `json:"description"` + Articles []Article `json:"articles" gorm:"many2many:article_tags"` + Moments []Moment `json:"moments" gorm:"many2many:moment_tags"` + Comments []Comment `json:"comments" gorm:"many2many:comment_tags"` } type Category struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` - Name string `json:"name"` - Description string `json:"description"` - Posts []Post `json:"categories" gorm:"many2many:post_categories"` + Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` + Name string `json:"name"` + Description string `json:"description"` + Articles []Article `json:"articles" gorm:"many2many:article_categories"` + Moments []Moment `json:"moments" gorm:"many2many:moment_categories"` + Comments []Comment `json:"comments" gorm:"many2many:comment_categories"` } diff --git a/pkg/models/comments.go b/pkg/models/comments.go new file mode 100644 index 0000000..8393786 --- /dev/null +++ b/pkg/models/comments.go @@ -0,0 +1,38 @@ +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"` + + ArticleID *uint `json:"article_id"` + MomentID *uint `json:"moment_id"` + Article *Article `json:"article"` + Moment *Moment `json:"moment"` +} + +func (p Comment) GetReplyTo() PostInterface { + return p.ReplyTo +} + +func (p Comment) GetHashtags() []Tag { + return p.Hashtags +} + +func (p Comment) GetCategories() []Category { + return p.Categories +} + +func (p Comment) SetHashtags(tags []Tag) { + p.Hashtags = tags +} + +func (p Comment) SetCategories(categories []Category) { + p.Categories = categories +} diff --git a/pkg/models/moments.go b/pkg/models/moments.go new file mode 100644 index 0000000..9312b94 --- /dev/null +++ b/pkg/models/moments.go @@ -0,0 +1,41 @@ +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"` + + Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"` +} + +func (p Moment) GetRepostTo() PostInterface { + return p.RepostTo +} + +func (p Moment) GetRealm() *Realm { + return p.Realm +} + +func (p Moment) GetHashtags() []Tag { + return p.Hashtags +} + +func (p Moment) GetCategories() []Category { + return p.Categories +} + +func (p Moment) SetHashtags(tags []Tag) { + p.Hashtags = tags +} + +func (p Moment) SetCategories(categories []Category) { + p.Categories = categories +} diff --git a/pkg/models/posts.go b/pkg/models/posts.go index cb8eedb..38c7752 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -1,26 +1,26 @@ package models -import "time" +import ( + "time" +) -type Post struct { +type PostReactInfo 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"` +} + +type PostBase struct { BaseModel - Content string `json:"content"` - Hashtags []Tag `json:"tags" gorm:"many2many:post_tags"` - Categories []Category `json:"categories" gorm:"many2many:post_categories"` - Attachments []Attachment `json:"attachments"` - LikedAccounts []PostLike `json:"liked_accounts"` - DislikedAccounts []PostDislike `json:"disliked_accounts"` - RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"` - ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"` - PinnedAt *time.Time `json:"pinned_at"` - EditedAt *time.Time `json:"edited_at"` - PublishedAt time.Time `json:"published_at"` - RepostID *uint `json:"repost_id"` - ReplyID *uint `json:"reply_id"` - RealmID *uint `json:"realm_id"` - AuthorID uint `json:"author_id"` - Author Account `json:"author"` + Alias string `json:"alias" gorm:"uniqueIndex"` + Attachments []Attachment `json:"attachments"` + PublishedAt *time.Time `json:"published_at"` + + AuthorID uint `json:"author_id"` + Author Account `json:"author"` // Dynamic Calculating Values LikeCount int64 `json:"like_count" gorm:"-"` @@ -28,3 +28,44 @@ type Post struct { ReplyCount int64 `json:"reply_count" gorm:"-"` RepostCount int64 `json:"repost_count" gorm:"-"` } + +func (p PostBase) GetID() uint { + return p.ID +} + +func (p PostBase) GetReplyTo() PostInterface { + return nil +} + +func (p PostBase) GetRepostTo() PostInterface { + return nil +} + +func (p PostBase) GetAuthor() Account { + return p.Author +} + +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 + GetCategories() []Category + GetReplyTo() PostInterface + GetRepostTo() PostInterface + GetAuthor() Account + GetRealm() *Realm + + SetHashtags([]Tag) + SetCategories([]Category) + SetReactInfo(PostReactInfo) +} diff --git a/pkg/models/reactions.go b/pkg/models/reactions.go index bb55844..d1a5385 100644 --- a/pkg/models/reactions.go +++ b/pkg/models/reactions.go @@ -2,18 +2,62 @@ package models import "time" -type PostLike struct { +type CommentLike struct { ID uint `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PostID uint `json:"post_id"` + ArticleID *uint `json:"article_id"` + MomentID *uint `json:"moment_id"` + CommentID *uint `json:"comment_id"` AccountID uint `json:"account_id"` } -type PostDislike struct { +type CommentDislike struct { ID uint `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - PostID uint `json:"post_id"` + 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 { + 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"` } diff --git a/pkg/models/realms.go b/pkg/models/realms.go index 9520098..4e5154b 100644 --- a/pkg/models/realms.go +++ b/pkg/models/realms.go @@ -5,7 +5,8 @@ type Realm struct { Name string `json:"name"` Description string `json:"description"` - Posts []Post `json:"posts"` + Articles []Article `json:"article"` + Moments []Moment `json:"moments"` Members []RealmMember `json:"members"` IsPublic bool `json:"is_public"` AccountID uint `json:"account_id"` diff --git a/pkg/server/categories_api.go b/pkg/server/categories_api.go index c5c00db..e16389c 100644 --- a/pkg/server/categories_api.go +++ b/pkg/server/categories_api.go @@ -6,7 +6,7 @@ import ( "github.com/gofiber/fiber/v2" ) -func listCategroies(c *fiber.Ctx) error { +func listCategories(c *fiber.Ctx) error { categories, err := services.ListCategory() if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) diff --git a/pkg/server/creators_api.go b/pkg/server/creators_api.go deleted file mode 100644 index 9d72759..0000000 --- a/pkg/server/creators_api.go +++ /dev/null @@ -1,93 +0,0 @@ -package server - -import ( - "code.smartsheep.studio/hydrogen/interactive/pkg/database" - "code.smartsheep.studio/hydrogen/interactive/pkg/models" - "code.smartsheep.studio/hydrogen/interactive/pkg/services" - "github.com/gofiber/fiber/v2" - "github.com/samber/lo" - "time" -) - -func getOwnPost(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - - id, _ := c.ParamsInt("postId", 0) - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - - tx := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }) - - post, err := services.GetPost(tx) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - tx = database.C. - Where(&models.Post{ReplyID: &post.ID}). - Where("published_at <= ? OR published_at IS NULL", time.Now()). - Order("created_at desc") - - var count int64 - if err := tx. - Model(&models.Post{}). - Count(&count).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - posts, err := services.ListPost(tx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(fiber.Map{ - "data": post, - "count": count, - "related": posts, - }) -} - -func listOwnPost(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - realmId := c.QueryInt("realmId", 0) - - user := c.Locals("principal").(models.Account) - - tx := database.C. - Where(&models.Post{AuthorID: user.ID}). - Where("published_at <= ? OR published_at IS NULL", time.Now()). - Order("created_at desc") - - if realmId > 0 { - tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))}) - } - - if len(c.Query("category")) > 0 { - tx = services.FilterPostWithCategory(tx, c.Query("category")) - } - - if len(c.Query("tag")) > 0 { - tx = services.FilterPostWithTag(tx, c.Query("tag")) - } - - var count int64 - if err := tx. - Model(&models.Post{}). - Count(&count).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - posts, err := services.ListPost(tx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(fiber.Map{ - "count": count, - "data": posts, - }) -} diff --git a/pkg/server/posts_api.go b/pkg/server/moments_api.go similarity index 51% rename from pkg/server/posts_api.go rename to pkg/server/moments_api.go index 48d9ef6..a25979a 100644 --- a/pkg/server/posts_api.go +++ b/pkg/server/moments_api.go @@ -12,105 +12,81 @@ import ( "github.com/samber/lo" ) -func getPost(c *fiber.Ctx) error { - id, _ := c.ParamsInt("postId", 0) - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) +func getMomentContext() *services.PostTypeContext[models.Moment] { + return &services.PostTypeContext[models.Moment]{ + Tx: database.C, + TypeName: "Moment", + CanReply: false, + CanRepost: true, + } +} - tx := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - }).Where("published_at <= ? OR published_at IS NULL", time.Now()) +func getMoment(c *fiber.Ctx) error { + id, _ := c.ParamsInt("momentId", 0) - post, err := services.GetPost(tx) + mx := getMomentContext().FilterPublishedAt(time.Now()) + + item, err := mx.Get(uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - tx = database.C. - Where(&models.Post{ReplyID: &post.ID}). - Where("published_at <= ? OR published_at IS NULL", time.Now()). - Order("created_at desc") - - var count int64 - if err := tx. - Model(&models.Post{}). - Count(&count).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - posts, err := services.ListPost(tx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(fiber.Map{ - "data": post, - "count": count, - "related": posts, - }) + return c.JSON(item) } -func listPost(c *fiber.Ctx) error { +func listMoment(c *fiber.Ctx) error { take := c.QueryInt("take", 0) offset := c.QueryInt("offset", 0) - realmId := c.QueryInt("realmId", 0) - tx := database.C. - Where("published_at <= ? OR published_at IS NULL", time.Now()). - Order("created_at desc") - - if realmId > 0 { - tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))}) - } else { - tx = tx.Where("realm_id IS NULL") - } + mx := getMomentContext(). + FilterPublishedAt(time.Now()). + FilterRealm(uint(realmId)). + SortCreatedAt("desc") var author models.Account if len(c.Query("authorId")) > 0 { if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - tx = tx.Where(&models.Post{AuthorID: author.ID}) + mx = mx.FilterAuthor(author.ID) } if len(c.Query("category")) > 0 { - tx = services.FilterPostWithCategory(tx, c.Query("category")) + mx = mx.FilterWithCategory(c.Query("category")) } if len(c.Query("tag")) > 0 { - tx = services.FilterPostWithTag(tx, c.Query("tag")) + mx = mx.FilterWithTag(c.Query("tag")) } if !c.QueryBool("reply", true) { - tx = tx.Where("reply_id IS NULL") + mx = mx.FilterReply(true) } - var count int64 - if err := tx. - Model(&models.Post{}). - Count(&count).Error; err != nil { + count, err := mx.Count() + if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - posts, err := services.ListPost(tx, take, offset) + items, err := mx.List(take, offset) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } return c.JSON(fiber.Map{ "count": count, - "data": posts, + "data": items, }) } -func createPost(c *fiber.Ctx) error { +func createMoment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) var data struct { Alias string `json:"alias"` Title string `json:"title"` Content string `json:"content" validate:"required"` - Tags []models.Tag `json:"tags"` + Hashtags []models.Tag `json:"hashtags"` Categories []models.Category `json:"categories"` Attachments []models.Attachment `json:"attachments"` PublishedAt *time.Time `json:"published_at"` @@ -125,28 +101,30 @@ func createPost(c *fiber.Ctx) error { data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") } - var repostTo *uint = nil - var replyTo *uint = nil + mx := getMomentContext() + + item := models.Moment{ + PostBase: models.PostBase{ + Alias: data.Alias, + Attachments: data.Attachments, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + }, + Hashtags: data.Hashtags, + Categories: data.Categories, + Content: data.Content, + RealmID: data.RealmID, + } + var relatedCount int64 if data.RepostTo > 0 { - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: data.RepostTo}, - }).Model(&models.Post{}).Count(&relatedCount).Error; err != nil { + if err := database.C.Where("id = ?", data.RepostTo). + Model(&models.Moment{}).Count(&relatedCount).Error; err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } else if relatedCount <= 0 { return fiber.NewError(fiber.StatusNotFound, "related post was not found") } else { - repostTo = &data.RepostTo - } - } else if data.ReplyTo > 0 { - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: data.ReplyTo}, - }).Model(&models.Post{}).Count(&relatedCount).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if relatedCount <= 0 { - return fiber.NewError(fiber.StatusNotFound, "related post was not found") - } else { - replyTo = &data.ReplyTo + item.RepostID = &data.RepostTo } } @@ -159,34 +137,23 @@ func createPost(c *fiber.Ctx) error { } } - post, err := services.NewPost( - user, - realm, - data.Content, - data.Attachments, - data.Categories, - data.Tags, - data.PublishedAt, - replyTo, - repostTo, - ) + item, err := mx.New(item) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return c.JSON(post) + return c.JSON(item) } -func editPost(c *fiber.Ctx) error { +func editMoment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("postId", 0) + id, _ := c.ParamsInt("momentId", 0) var data struct { Alias string `json:"alias" validate:"required"` - Title string `json:"title"` Content string `json:"content" validate:"required"` PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` + Hashtags []models.Tag `json:"hashtags"` Categories []models.Category `json:"categories"` Attachments []models.Attachment `json:"attachments"` } @@ -195,49 +162,48 @@ func editPost(c *fiber.Ctx) error { return err } - var post models.Post - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }).First(&post).Error; err != nil { + mx := getMomentContext().FilterAuthor(user.ID) + + item, err := mx.Get(uint(id), true) + if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - post, err := services.EditPost( - post, - data.Content, - data.PublishedAt, - data.Categories, - data.Tags, - data.Attachments, - ) + item.Alias = data.Alias + item.Content = data.Content + item.PublishedAt = data.PublishedAt + item.Hashtags = data.Hashtags + item.Categories = data.Categories + item.Attachments = data.Attachments + + item, err = mx.Edit(item) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return c.JSON(post) + return c.JSON(item) } -func reactPost(c *fiber.Ctx) error { +func reactMoment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("postId", 0) + id, _ := c.ParamsInt("momentId", 0) - var post models.Post - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - }).First(&post).Error; err != nil { + mx := getMomentContext() + + item, err := mx.Get(uint(id), true) + if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } switch strings.ToLower(c.Params("reactType")) { case "like": - if positive, err := services.LikePost(user, post); err != nil { + 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 := services.DislikePost(user, post); err != nil { + 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)) @@ -247,19 +213,18 @@ func reactPost(c *fiber.Ctx) error { } } -func deletePost(c *fiber.Ctx) error { +func deleteMoment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("postId", 0) + id, _ := c.ParamsInt("momentId", 0) - var post models.Post - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }).First(&post).Error; err != nil { + mx := getMomentContext().FilterAuthor(user.ID) + + item, err := mx.Get(uint(id), true) + if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - if err := services.DeletePost(post); err != nil { + if err := mx.Delete(item); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 398a432..9087efc 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -69,21 +69,23 @@ func NewServer() { }), openAttachment) api.Post("/attachments", authMiddleware, uploadAttachment) - api.Get("/posts", listPost) - api.Get("/posts/:postId", getPost) - api.Post("/posts", authMiddleware, createPost) - api.Post("/posts/:postId/react/:reactType", authMiddleware, reactPost) - api.Put("/posts/:postId", authMiddleware, editPost) - api.Delete("/posts/:postId", authMiddleware, deletePost) + // TODO Feed (aka. Union source) - api.Get("/categories", listCategroies) + moments := api.Group("/moments").Name("Moments API") + { + moments.Get("/", listMoment) + moments.Get("/:momentId", getMoment) + moments.Post("/", authMiddleware, createMoment) + moments.Post("/:momentId/react/:reactType", authMiddleware, reactMoment) + moments.Put("/:momentId", authMiddleware, editMoment) + moments.Delete("/:momentId", authMiddleware, deleteMoment) + } + + api.Get("/categories", listCategories) api.Post("/categories", authMiddleware, newCategory) api.Put("/categories/:categoryId", authMiddleware, editCategory) api.Delete("/categories/:categoryId", authMiddleware, deleteCategory) - api.Get("/creators/posts", authMiddleware, listOwnPost) - api.Get("/creators/posts/:postId", authMiddleware, getOwnPost) - api.Get("/realms", listRealm) api.Get("/realms/me", authMiddleware, listOwnedRealm) api.Get("/realms/me/available", authMiddleware, listAvailableRealm) diff --git a/pkg/services/attachments.go b/pkg/services/attachments.go index d8c893d..2b04a46 100644 --- a/pkg/services/attachments.go +++ b/pkg/services/attachments.go @@ -14,7 +14,7 @@ func NewAttachment(user models.Account, header *multipart.FileHeader) (models.At Filesize: header.Size, Filename: header.Filename, Mimetype: "unknown/unknown", - PostID: nil, + Type: models.AttachmentOthers, AuthorID: user.ID, } diff --git a/pkg/services/moments.go b/pkg/services/moments.go new file mode 100644 index 0000000..5e568ea --- /dev/null +++ b/pkg/services/moments.go @@ -0,0 +1 @@ +package services diff --git a/pkg/services/posts.go b/pkg/services/posts.go index 84df779..abd8dc4 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -2,112 +2,191 @@ package services import ( "code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto" - "errors" - "fmt" - "time" - - "github.com/rs/zerolog/log" - "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "fmt" + pluralize "github.com/gertd/go-pluralize" + "github.com/rs/zerolog/log" "github.com/samber/lo" "github.com/spf13/viper" "gorm.io/gorm" + "strings" + "time" ) -func PreloadRelatedPost(tx *gorm.DB) *gorm.DB { - return tx. - Preload("Author"). - Preload("Attachments"). - Preload("Categories"). - Preload("Hashtags"). - Preload("RepostTo"). - Preload("ReplyTo"). - Preload("RepostTo.Author"). - Preload("ReplyTo.Author"). - Preload("RepostTo.Attachments"). - Preload("ReplyTo.Attachments"). - Preload("RepostTo.Categories"). - Preload("ReplyTo.Categories"). - Preload("RepostTo.Hashtags"). - Preload("ReplyTo.Hashtags") -} - -func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB { - prefix := viper.GetString("database.prefix") - return tx.Joins(fmt.Sprintf("JOIN %spost_categories ON %sposts.id = %spost_categories.post_id", prefix, prefix, prefix)). - Joins(fmt.Sprintf("JOIN %scategories ON %scategories.id = %spost_categories.category_id", prefix, prefix, prefix)). - Where(fmt.Sprintf("%scategories.alias = ?", prefix), alias) -} - -func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB { - prefix := viper.GetString("database.prefix") - return tx.Joins(fmt.Sprintf("JOIN %spost_tags ON %sposts.id = %spost_tags.post_id", prefix, prefix, prefix)). - Joins(fmt.Sprintf("JOIN %stags ON %stags.id = %spost_tags.tag_id", prefix, prefix, prefix)). - Where(fmt.Sprintf("%stags.alias = ?", prefix), alias) -} - -func GetPost(tx *gorm.DB) (*models.Post, error) { - var post *models.Post - if err := PreloadRelatedPost(tx).First(&post).Error; err != nil { - return post, err - } - - 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"` - } - - prefix := viper.GetString("database.prefix") - database.C.Raw(fmt.Sprintf(` -SELECT t.id as post_id, +const ( + reactUnionSelect = `SELECT t.id AS post_id, COALESCE(l.like_count, 0) AS like_count, - COALESCE(d.dislike_count, 0) AS dislike_count, - COALESCE(r.reply_count, 0) AS reply_count, - COALESCE(rp.repost_count, 0) AS repost_count -FROM %sposts t - LEFT JOIN (SELECT post_id, COUNT(*) AS like_count - FROM %spost_likes - GROUP BY post_id) l ON t.id = l.post_id - LEFT JOIN (SELECT post_id, COUNT(*) AS dislike_count - FROM %spost_dislikes - GROUP BY post_id) d ON t.id = d.post_id - LEFT JOIN (SELECT reply_id, COUNT(*) AS reply_count - FROM %sposts - WHERE reply_id IS NOT NULL - GROUP BY reply_id) r ON t.id = r.reply_id - LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count - FROM %sposts - WHERE repost_id IS NOT NULL - GROUP BY repost_id) rp ON t.id = rp.repost_id -WHERE t.id = ?`, prefix, prefix, prefix, prefix, prefix), post.ID).Scan(&reactInfo) + 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` +) - post.LikeCount = reactInfo.LikeCount - post.DislikeCount = reactInfo.DislikeCount - post.ReplyCount = reactInfo.ReplyCount - post.RepostCount = reactInfo.RepostCount +type PostTypeContext[T models.PostInterface] struct { + Tx *gorm.DB - return post, nil + TypeName string + CanReply bool + CanRepost bool } -func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) { +var pluralizeHelper = pluralize.NewClient() + +func (v *PostTypeContext[T]) GetTableName(plural ...bool) string { + if len(plural) <= 0 || !plural[0] { + return strings.ToLower(v.TypeName) + } else { + return pluralizeHelper.Plural(strings.ToLower(v.TypeName)) + } +} + +func (v *PostTypeContext[T]) Preload() *PostTypeContext[T] { + v.Tx.Preload("Author").Preload("Attachments").Preload("Categories").Preload("Hashtags") + + if v.CanReply { + v.Tx.Preload("ReplyTo") + } + if v.CanRepost { + v.Tx.Preload("RepostTo") + } + + return v +} + +func (v *PostTypeContext[T]) FilterWithCategory(alias string) *PostTypeContext[T] { + table := v.GetTableName() + v.Tx.Joins(fmt.Sprintf("JOIN %s_categories ON %s.id = %s_categories.%s_id", table, v.GetTableName(true), table, v.GetTableName())). + Joins(fmt.Sprintf("JOIN %s_categories ON %s_categories.id = %s_categories.category_id", table, table, table)). + Where(table+"_categories.alias = ?", alias) + return v +} + +func (v *PostTypeContext[T]) FilterWithTag(alias string) *PostTypeContext[T] { + table := v.GetTableName() + v.Tx.Joins(fmt.Sprintf("JOIN %s_tags ON %s.id = %s_tags.%s_id", table, v.GetTableName(true), table, v.GetTableName())). + Joins(fmt.Sprintf("JOIN %s_tags ON %s_tags.id = %s_tags.category_id", table, table, table)). + Where(table+"_tags.alias = ?", alias) + return v +} + +func (v *PostTypeContext[T]) FilterPublishedAt(date time.Time) *PostTypeContext[T] { + v.Tx.Where("published_at <= ? AND published_at IS NULL", date) + return v +} + +func (v *PostTypeContext[T]) FilterRealm(id uint) *PostTypeContext[T] { + if id > 0 { + v.Tx = v.Tx.Where("realm_id = ?", id) + } else { + v.Tx = v.Tx.Where("realm_id IS NULL") + } + return v +} + +func (v *PostTypeContext[T]) FilterAuthor(id uint) *PostTypeContext[T] { + v.Tx = v.Tx.Where("author_id = ?", id) + return v +} + +func (v *PostTypeContext[T]) FilterReply(condition bool) *PostTypeContext[T] { + if condition { + v.Tx = v.Tx.Where("reply_id IS NOT NULL") + } else { + v.Tx = v.Tx.Where("reply_id IS NULL") + } + return v +} + +func (v *PostTypeContext[T]) SortCreatedAt(order string) *PostTypeContext[T] { + v.Tx.Order(fmt.Sprintf("created_at %s", order)) + 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) { + 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 +} + +func (v *PostTypeContext[T]) Count() (int64, error) { + var count int64 + table := viper.GetString("database.prefix") + v.GetTableName(true) + if err := v.Tx.Table(table).Count(&count).Error; err != nil { + return count, err + } + + return count, nil +} + +func (v *PostTypeContext[T]) List(take int, offset int, noReact ...bool) ([]T, error) { if take > 20 { take = 20 } - var posts []*models.Post - if err := PreloadRelatedPost(tx). - Limit(take). - Offset(offset). - Find(&posts).Error; err != nil { - return posts, err + var items []T + if err := v.Preload().Tx.Limit(take).Offset(offset).Find(&items).Error; err != nil { + return items, err } - postIds := lo.Map(posts, func(item *models.Post, _ int) uint { - return item.ID + idx := lo.Map(items, func(item T, _ int) uint { + return item.GetID() }) var reactInfo []struct { @@ -118,229 +197,155 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) { RepostCount int64 `json:"repost_count"` } - prefix := viper.GetString("database.prefix") - database.C.Raw(fmt.Sprintf(` -SELECT t.id as post_id, - COALESCE(l.like_count, 0) AS like_count, - COALESCE(d.dislike_count, 0) AS dislike_count, - COALESCE(r.reply_count, 0) AS reply_count, - COALESCE(rp.repost_count, 0) AS repost_count -FROM %sposts t - LEFT JOIN (SELECT post_id, COUNT(*) AS like_count - FROM %spost_likes - GROUP BY post_id) l ON t.id = l.post_id - LEFT JOIN (SELECT post_id, COUNT(*) AS dislike_count - FROM %spost_dislikes - GROUP BY post_id) d ON t.id = d.post_id - LEFT JOIN (SELECT reply_id, COUNT(*) AS reply_count - FROM %sposts - WHERE reply_id IS NOT NULL - GROUP BY reply_id) r ON t.id = r.reply_id - LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count - FROM %sposts - WHERE repost_id IS NOT NULL - GROUP BY repost_id) rp ON t.id = rp.repost_id -WHERE t.id IN ?`, prefix, prefix, prefix, prefix, prefix), postIds).Scan(&reactInfo) + if len(noReact) <= 0 || !noReact[0] { + sql := v.BuildReactInfoSql() + database.C.Raw(sql, idx).Scan(&reactInfo) + } - postMap := lo.SliceToMap(posts, func(item *models.Post) (uint, *models.Post) { - return item.ID, item + itemMap := lo.SliceToMap(items, func(item T) (uint, T) { + return item.GetID(), item }) for _, info := range reactInfo { - if post, ok := postMap[info.PostID]; ok { - post.LikeCount = info.LikeCount - post.DislikeCount = info.DislikeCount - post.ReplyCount = info.ReplyCount - post.RepostCount = info.RepostCount + if item, ok := itemMap[info.PostID]; ok { + item.SetReactInfo(info) } } - return posts, nil + return items, nil } -func NewPost( - user models.Account, - realm *models.Realm, - content string, - attachments []models.Attachment, - categories []models.Category, - tags []models.Tag, - publishedAt *time.Time, - replyTo, repostTo *uint, -) (models.Post, error) { +func (v *PostTypeContext[T]) MapCategoriesAndTags(item T) (T, error) { var err error - var post models.Post + categories := item.GetCategories() for idx, category := range categories { categories[idx], err = GetCategory(category.Alias) if err != nil { - return post, err + return item, err } } + item.SetCategories(categories) + tags := item.GetHashtags() for idx, tag := range tags { tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) if err != nil { - return post, err + return item, err } } + item.SetHashtags(tags) + return item, nil +} - var realmId *uint - if realm != nil { - if !realm.IsPublic { +func (v *PostTypeContext[T]) New(item T) (T, error) { + item, err := v.MapCategoriesAndTags(item) + if err != nil { + return item, err + } + + if item.GetRealm() != nil { + if !item.GetRealm().IsPublic { var member models.RealmMember if err := database.C.Where(&models.RealmMember{ - RealmID: realm.ID, - AccountID: user.ID, + RealmID: item.GetRealm().ID, + AccountID: item.GetAuthor().ID, }).First(&member).Error; err != nil { - return post, fmt.Errorf("you aren't a part of that realm") + return item, fmt.Errorf("you aren't a part of that realm") } } - realmId = &realm.ID } - if publishedAt == nil { - publishedAt = lo.ToPtr(time.Now()) + if err := database.C.Save(&item).Error; err != nil { + return item, err } - post = models.Post{ - Content: content, - Attachments: attachments, - Hashtags: tags, - Categories: categories, - AuthorID: user.ID, - RealmID: realmId, - PublishedAt: *publishedAt, - RepostID: repostTo, - ReplyID: replyTo, + if item.GetReplyTo() != nil { + go func() { + var op models.Moment + if err := database.C.Where("id = ?", item.GetReplyTo()).Preload("Author").First(&op).Error; err == nil { + if op.Author.ID != item.GetAuthor().ID { + postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) + err := NotifyAccount( + op.Author, + fmt.Sprintf("%s replied you", item.GetAuthor().Name), + fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name), + &proto.NotifyLink{Label: "Related post", Url: postUrl}, + ) + if err != nil { + log.Error().Err(err).Msg("An error occurred when notifying user...") + } + } + } + }() } - if err := database.C.Save(&post).Error; err != nil { - return post, err - } + var subscribers []models.AccountMembership + if err := database.C.Where(&models.AccountMembership{ + FollowingID: item.GetAuthor().ID, + }).Preload("Follower").Find(&subscribers).Error; err == nil && len(subscribers) > 0 { + go func() { + accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account { + return item.Follower + }) - if post.ReplyID != nil { - var op models.Post - if err := database.C.Where(&models.Post{ - BaseModel: models.BaseModel{ID: *post.ReplyID}, - }).Preload("Author").First(&op).Error; err == nil { - if op.Author.ID != user.ID { - postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID) + for _, account := range accounts { + postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) err := NotifyAccount( - op.Author, - fmt.Sprintf("%s replied you", user.Name), - fmt.Sprintf("%s replied your post. Check it out!", user.Name), + account, + fmt.Sprintf("%s just posted a post", item.GetAuthor().Name), + "Account you followed post a brand new post. Check it out!", &proto.NotifyLink{Label: "Related post", Url: postUrl}, ) if err != nil { log.Error().Err(err).Msg("An error occurred when notifying user...") } } - } + }() } - go func() { - var subscribers []models.AccountMembership - if err := database.C.Where(&models.AccountMembership{ - FollowingID: user.ID, - }).Preload("Follower").Find(&subscribers).Error; err != nil { - return - } - - accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account { - return item.Follower - }) - - for _, account := range accounts { - postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID) - err := NotifyAccount( - account, - fmt.Sprintf("%s just posted a post", user.Name), - "Account you followed post a brand new post. Check it out!", - &proto.NotifyLink{Label: "Related post", Url: postUrl}, - ) - if err != nil { - log.Error().Err(err).Msg("An error occurred when notifying user...") - } - } - }() - - return post, nil + return item, nil } -func EditPost( - post models.Post, - content string, - publishedAt *time.Time, - categories []models.Category, - tags []models.Tag, - attachments []models.Attachment, -) (models.Post, error) { - var err error - for idx, category := range categories { - categories[idx], err = GetCategory(category.Alias) - if err != nil { - return post, err - } - } - for idx, tag := range tags { - tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) - if err != nil { - return post, err - } +func (v *PostTypeContext[T]) Edit(item T) (T, error) { + item, err := v.MapCategoriesAndTags(item) + if err != nil { + return item, err } - if publishedAt == nil { - publishedAt = lo.ToPtr(time.Now()) - } + err = database.C.Save(&item).Error - post.Content = content - post.PublishedAt = *publishedAt - post.Hashtags = tags - post.Categories = categories - post.Attachments = attachments - - err = database.C.Save(&post).Error - - return post, err + return item, err } -func LikePost(user models.Account, post models.Post) (bool, error) { - var like models.PostLike - if err := database.C.Where(&models.PostLike{ - AccountID: user.ID, - PostID: post.ID, - }).First(&like).Error; err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return true, err - } - like = models.PostLike{ - AccountID: user.ID, - PostID: post.ID, - } - return true, database.C.Save(&like).Error +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 } else { - return false, database.C.Delete(&like).Error + 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 DislikePost(user models.Account, post models.Post) (bool, error) { - var dislike models.PostDislike - if err := database.C.Where(&models.PostDislike{ - AccountID: user.ID, - PostID: post.ID, - }).First(&dislike).Error; err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return true, err - } - dislike = models.PostDislike{ - AccountID: user.ID, - PostID: post.ID, - } - return true, database.C.Save(&dislike).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 { - return false, database.C.Delete(&dislike).Error + 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 DeletePost(post models.Post) error { - return database.C.Delete(&post).Error -}