♻️ Interactive v2 #1

Merged
LittleSheep merged 30 commits from refactor/v2 into master 2024-03-16 08:22:25 +00:00
19 changed files with 649 additions and 534 deletions
Showing only changes of commit c23bde901b - Show all commits

1
go.mod
View File

@ -24,6 +24,7 @@ require (
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect

2
go.sum
View File

@ -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/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 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=

View File

@ -13,9 +13,15 @@ func RunMigration(source *gorm.DB) error {
&models.RealmMember{}, &models.RealmMember{},
&models.Category{}, &models.Category{},
&models.Tag{}, &models.Tag{},
&models.Post{}, &models.Moment{},
&models.PostLike{}, &models.MomentLike{},
&models.PostDislike{}, &models.MomentDislike{},
&models.Article{},
&models.ArticleLike{},
&models.ArticleDislike{},
&models.Comment{},
&models.CommentLike{},
&models.CommentDislike{},
&models.Attachment{}, &models.Attachment{},
); err != nil { ); err != nil {
return err return err

View File

@ -14,10 +14,15 @@ type Account struct {
Description string `json:"description"` Description string `json:"description"`
EmailAddress string `json:"email_address"` EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"` PowerLevel int `json:"power_level"`
Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` Moments []Moment `json:"moments" gorm:"foreignKey:AuthorID"`
Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"`
Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"`
LikedPosts []PostLike `json:"liked_posts"` LikedMoments []MomentLike `json:"liked_moments"`
DislikedPosts []PostDislike `json:"disliked_posts"` 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"` RealmIdentities []RealmMember `json:"identities"`
Realms []Realm `json:"realms"` Realms []Realm `json:"realms"`
ExternalID uint `json:"external_id"` ExternalID uint `json:"external_id"`

41
pkg/models/articles.go Normal file
View File

@ -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
}

View File

@ -6,6 +6,15 @@ import (
"path/filepath" "path/filepath"
) )
type AttachmentType = uint8
const (
AttachmentOthers = AttachmentType(iota)
AttachmentPhoto
AttachmentVideo
AttachmentAudio
)
type Attachment struct { type Attachment struct {
BaseModel BaseModel
@ -13,10 +22,12 @@ type Attachment struct {
Filesize int64 `json:"filesize"` Filesize int64 `json:"filesize"`
Filename string `json:"filename"` Filename string `json:"filename"`
Mimetype string `json:"mimetype"` Mimetype string `json:"mimetype"`
Type AttachmentType `json:"type"`
ExternalUrl string `json:"external_url"` ExternalUrl string `json:"external_url"`
Post *Post `json:"post"`
Author Account `json:"author"` Author Account `json:"author"`
PostID *uint `json:"post_id"` ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
CommentID *uint `json:"comment_id"`
AuthorID uint `json:"author_id"` AuthorID uint `json:"author_id"`
} }

View File

@ -6,7 +6,9 @@ type Tag struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Posts []Post `json:"posts" gorm:"many2many:post_tags"` 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 { type Category struct {
@ -15,5 +17,7 @@ type Category struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Posts []Post `json:"categories" gorm:"many2many:post_categories"` Articles []Article `json:"articles" gorm:"many2many:article_categories"`
Moments []Moment `json:"moments" gorm:"many2many:moment_categories"`
Comments []Comment `json:"comments" gorm:"many2many:comment_categories"`
} }

38
pkg/models/comments.go Normal file
View File

@ -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
}

41
pkg/models/moments.go Normal file
View File

@ -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
}

View File

@ -1,24 +1,24 @@
package models 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 BaseModel
Content string `json:"content"` Alias string `json:"alias" gorm:"uniqueIndex"`
Hashtags []Tag `json:"tags" gorm:"many2many:post_tags"`
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
Attachments []Attachment `json:"attachments"` Attachments []Attachment `json:"attachments"`
LikedAccounts []PostLike `json:"liked_accounts"` PublishedAt *time.Time `json:"published_at"`
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"` AuthorID uint `json:"author_id"`
Author Account `json:"author"` Author Account `json:"author"`
@ -28,3 +28,44 @@ type Post struct {
ReplyCount int64 `json:"reply_count" gorm:"-"` ReplyCount int64 `json:"reply_count" gorm:"-"`
RepostCount int64 `json:"repost_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)
}

View File

@ -2,18 +2,62 @@ package models
import "time" import "time"
type PostLike struct { type CommentLike struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` AccountID uint `json:"account_id"`
} }
type PostDislike struct { type CommentDislike struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_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"` AccountID uint `json:"account_id"`
} }

View File

@ -5,7 +5,8 @@ type Realm struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Posts []Post `json:"posts"` Articles []Article `json:"article"`
Moments []Moment `json:"moments"`
Members []RealmMember `json:"members"` Members []RealmMember `json:"members"`
IsPublic bool `json:"is_public"` IsPublic bool `json:"is_public"`
AccountID uint `json:"account_id"` AccountID uint `json:"account_id"`

View File

@ -6,7 +6,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func listCategroies(c *fiber.Ctx) error { func listCategories(c *fiber.Ctx) error {
categories, err := services.ListCategory() categories, err := services.ListCategory()
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())

View File

@ -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,
})
}

View File

@ -12,105 +12,81 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
func getPost(c *fiber.Ctx) error { func getMomentContext() *services.PostTypeContext[models.Moment] {
id, _ := c.ParamsInt("postId", 0) return &services.PostTypeContext[models.Moment]{
take := c.QueryInt("take", 0) Tx: database.C,
offset := c.QueryInt("offset", 0) TypeName: "Moment",
CanReply: false,
CanRepost: true,
}
}
tx := database.C.Where(&models.Post{ func getMoment(c *fiber.Ctx) error {
BaseModel: models.BaseModel{ID: uint(id)}, id, _ := c.ParamsInt("momentId", 0)
}).Where("published_at <= ? OR published_at IS NULL", time.Now())
post, err := services.GetPost(tx) mx := getMomentContext().FilterPublishedAt(time.Now())
item, err := mx.Get(uint(id))
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
tx = database.C. return c.JSON(item)
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 listPost(c *fiber.Ctx) error { func listMoment(c *fiber.Ctx) error {
take := c.QueryInt("take", 0) take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
realmId := c.QueryInt("realmId", 0) realmId := c.QueryInt("realmId", 0)
tx := database.C. mx := getMomentContext().
Where("published_at <= ? OR published_at IS NULL", time.Now()). FilterPublishedAt(time.Now()).
Order("created_at desc") FilterRealm(uint(realmId)).
SortCreatedAt("desc")
if realmId > 0 {
tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))})
} else {
tx = tx.Where("realm_id IS NULL")
}
var author models.Account var author models.Account
if len(c.Query("authorId")) > 0 { if len(c.Query("authorId")) > 0 {
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) 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 { 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 { if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag")) mx = mx.FilterWithTag(c.Query("tag"))
} }
if !c.QueryBool("reply", true) { if !c.QueryBool("reply", true) {
tx = tx.Where("reply_id IS NULL") mx = mx.FilterReply(true)
} }
var count int64 count, err := mx.Count()
if err := tx. if err != nil {
Model(&models.Post{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
posts, err := services.ListPost(tx, take, offset) items, err := mx.List(take, offset)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"count": count, "count": count,
"data": posts, "data": items,
}) })
} }
func createPost(c *fiber.Ctx) error { func createMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data struct { var data struct {
Alias string `json:"alias"` Alias string `json:"alias"`
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content" validate:"required"` Content string `json:"content" validate:"required"`
Tags []models.Tag `json:"tags"` Hashtags []models.Tag `json:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at"`
@ -125,28 +101,30 @@ func createPost(c *fiber.Ctx) error {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
} }
var repostTo *uint = nil mx := getMomentContext()
var replyTo *uint = nil
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 var relatedCount int64
if data.RepostTo > 0 { if data.RepostTo > 0 {
if err := database.C.Where(&models.Post{ if err := database.C.Where("id = ?", data.RepostTo).
BaseModel: models.BaseModel{ID: data.RepostTo}, Model(&models.Moment{}).Count(&relatedCount).Error; err != nil {
}).Model(&models.Post{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 { } else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found") return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else { } else {
repostTo = &data.RepostTo item.RepostID = &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
} }
} }
@ -159,34 +137,23 @@ func createPost(c *fiber.Ctx) error {
} }
} }
post, err := services.NewPost( item, err := mx.New(item)
user,
realm,
data.Content,
data.Attachments,
data.Categories,
data.Tags,
data.PublishedAt,
replyTo,
repostTo,
)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) 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) user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0) id, _ := c.ParamsInt("momentId", 0)
var data struct { var data struct {
Alias string `json:"alias" validate:"required"` Alias string `json:"alias" validate:"required"`
Title string `json:"title"`
Content string `json:"content" validate:"required"` Content string `json:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at"` PublishedAt *time.Time `json:"published_at"`
Tags []models.Tag `json:"tags"` Hashtags []models.Tag `json:"hashtags"`
Categories []models.Category `json:"categories"` Categories []models.Category `json:"categories"`
Attachments []models.Attachment `json:"attachments"` Attachments []models.Attachment `json:"attachments"`
} }
@ -195,49 +162,48 @@ func editPost(c *fiber.Ctx) error {
return err return err
} }
var post models.Post mx := getMomentContext().FilterAuthor(user.ID)
if err := database.C.Where(&models.Post{
BaseModel: models.BaseModel{ID: uint(id)}, item, err := mx.Get(uint(id), true)
AuthorID: user.ID, if err != nil {
}).First(&post).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
post, err := services.EditPost( item.Alias = data.Alias
post, item.Content = data.Content
data.Content, item.PublishedAt = data.PublishedAt
data.PublishedAt, item.Hashtags = data.Hashtags
data.Categories, item.Categories = data.Categories
data.Tags, item.Attachments = data.Attachments
data.Attachments,
) item, err = mx.Edit(item)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error()) 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) user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0) id, _ := c.ParamsInt("momentId", 0)
var post models.Post mx := getMomentContext()
if err := database.C.Where(&models.Post{
BaseModel: models.BaseModel{ID: uint(id)}, item, err := mx.Get(uint(id), true)
}).First(&post).Error; err != nil { if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
switch strings.ToLower(c.Params("reactType")) { switch strings.ToLower(c.Params("reactType")) {
case "like": 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()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else { } else {
return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent))
} }
case "dislike": 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()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else { } else {
return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) 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) user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0) id, _ := c.ParamsInt("momentId", 0)
var post models.Post mx := getMomentContext().FilterAuthor(user.ID)
if err := database.C.Where(&models.Post{
BaseModel: models.BaseModel{ID: uint(id)}, item, err := mx.Get(uint(id), true)
AuthorID: user.ID, if err != nil {
}).First(&post).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) 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()) return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }

View File

@ -69,21 +69,23 @@ func NewServer() {
}), openAttachment) }), openAttachment)
api.Post("/attachments", authMiddleware, uploadAttachment) api.Post("/attachments", authMiddleware, uploadAttachment)
api.Get("/posts", listPost) // TODO Feed (aka. Union source)
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)
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.Post("/categories", authMiddleware, newCategory)
api.Put("/categories/:categoryId", authMiddleware, editCategory) api.Put("/categories/:categoryId", authMiddleware, editCategory)
api.Delete("/categories/:categoryId", authMiddleware, deleteCategory) 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", listRealm)
api.Get("/realms/me", authMiddleware, listOwnedRealm) api.Get("/realms/me", authMiddleware, listOwnedRealm)
api.Get("/realms/me/available", authMiddleware, listAvailableRealm) api.Get("/realms/me/available", authMiddleware, listAvailableRealm)

View File

@ -14,7 +14,7 @@ func NewAttachment(user models.Account, header *multipart.FileHeader) (models.At
Filesize: header.Size, Filesize: header.Size,
Filename: header.Filename, Filename: header.Filename,
Mimetype: "unknown/unknown", Mimetype: "unknown/unknown",
PostID: nil, Type: models.AttachmentOthers,
AuthorID: user.ID, AuthorID: user.ID,
} }

1
pkg/services/moments.go Normal file
View File

@ -0,0 +1 @@
package services

View File

@ -2,112 +2,191 @@ package services
import ( import (
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto" "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/database"
"code.smartsheep.studio/hydrogen/interactive/pkg/models" "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/samber/lo"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/gorm" "gorm.io/gorm"
"strings"
"time"
) )
func PreloadRelatedPost(tx *gorm.DB) *gorm.DB { const (
return tx. reactUnionSelect = `SELECT t.id AS post_id,
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,
COALESCE(l.like_count, 0) AS like_count, COALESCE(l.like_count, 0) AS like_count,
COALESCE(d.dislike_count, 0) AS dislike_count, COALESCE(d.dislike_count, 0) AS dislike_count--!COMMA!--
COALESCE(r.reply_count, 0) AS reply_count, --!REPLY_UNION_COLUMN!-- --!BOTH_COMMA!--
COALESCE(rp.repost_count, 0) AS repost_count --!REPOST_UNION_COLUMN!--
FROM %sposts t FROM %s t
LEFT JOIN (SELECT post_id, COUNT(*) AS like_count LEFT JOIN (SELECT %s_id, COUNT(*) AS like_count
FROM %spost_likes FROM %s_likes
GROUP BY post_id) l ON t.id = l.post_id GROUP BY %s_id) l ON t.id = l.%s_id
LEFT JOIN (SELECT post_id, COUNT(*) AS dislike_count LEFT JOIN (SELECT %s_id, COUNT(*) AS dislike_count
FROM %spost_dislikes FROM %s_likes
GROUP BY post_id) d ON t.id = d.post_id GROUP BY %s_id) d ON t.id = d.%s_id
LEFT JOIN (SELECT reply_id, COUNT(*) AS reply_count --!REPLY_UNION_SELECT!--
FROM %sposts --!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 WHERE reply_id IS NOT NULL
GROUP BY reply_id) r ON t.id = r.reply_id GROUP BY reply_id) r ON t.id = r.reply_id`
LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count repostUnionColumn = `COALESCE(rp.repost_count, 0) AS repost_count`
FROM %sposts repostUnionSelect = `LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count
FROM %s
WHERE repost_id IS NOT NULL WHERE repost_id IS NOT NULL
GROUP BY repost_id) rp ON t.id = rp.repost_id GROUP BY repost_id) rp ON t.id = rp.repost_id`
WHERE t.id = ?`, prefix, prefix, prefix, prefix, prefix), post.ID).Scan(&reactInfo) )
post.LikeCount = reactInfo.LikeCount type PostTypeContext[T models.PostInterface] struct {
post.DislikeCount = reactInfo.DislikeCount Tx *gorm.DB
post.ReplyCount = reactInfo.ReplyCount
post.RepostCount = reactInfo.RepostCount
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 { if take > 20 {
take = 20 take = 20
} }
var posts []*models.Post var items []T
if err := PreloadRelatedPost(tx). if err := v.Preload().Tx.Limit(take).Offset(offset).Find(&items).Error; err != nil {
Limit(take). return items, err
Offset(offset).
Find(&posts).Error; err != nil {
return posts, err
} }
postIds := lo.Map(posts, func(item *models.Post, _ int) uint { idx := lo.Map(items, func(item T, _ int) uint {
return item.ID return item.GetID()
}) })
var reactInfo []struct { var reactInfo []struct {
@ -118,116 +197,77 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) {
RepostCount int64 `json:"repost_count"` RepostCount int64 `json:"repost_count"`
} }
prefix := viper.GetString("database.prefix") if len(noReact) <= 0 || !noReact[0] {
database.C.Raw(fmt.Sprintf(` sql := v.BuildReactInfoSql()
SELECT t.id as post_id, database.C.Raw(sql, idx).Scan(&reactInfo)
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)
postMap := lo.SliceToMap(posts, func(item *models.Post) (uint, *models.Post) { itemMap := lo.SliceToMap(items, func(item T) (uint, T) {
return item.ID, item return item.GetID(), item
}) })
for _, info := range reactInfo { for _, info := range reactInfo {
if post, ok := postMap[info.PostID]; ok { if item, ok := itemMap[info.PostID]; ok {
post.LikeCount = info.LikeCount item.SetReactInfo(info)
post.DislikeCount = info.DislikeCount
post.ReplyCount = info.ReplyCount
post.RepostCount = info.RepostCount
} }
} }
return posts, nil return items, nil
} }
func NewPost( func (v *PostTypeContext[T]) MapCategoriesAndTags(item T) (T, error) {
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) {
var err error var err error
var post models.Post categories := item.GetCategories()
for idx, category := range categories { for idx, category := range categories {
categories[idx], err = GetCategory(category.Alias) categories[idx], err = GetCategory(category.Alias)
if err != nil { if err != nil {
return post, err return item, err
} }
} }
item.SetCategories(categories)
tags := item.GetHashtags()
for idx, tag := range tags { for idx, tag := range tags {
tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
if err != nil { if err != nil {
return post, err return item, err
} }
} }
item.SetHashtags(tags)
return item, nil
}
func (v *PostTypeContext[T]) New(item T) (T, error) {
item, err := v.MapCategoriesAndTags(item)
if err != nil {
return item, err
}
var realmId *uint if item.GetRealm() != nil {
if realm != nil { if !item.GetRealm().IsPublic {
if !realm.IsPublic {
var member models.RealmMember var member models.RealmMember
if err := database.C.Where(&models.RealmMember{ if err := database.C.Where(&models.RealmMember{
RealmID: realm.ID, RealmID: item.GetRealm().ID,
AccountID: user.ID, AccountID: item.GetAuthor().ID,
}).First(&member).Error; err != nil { }).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 { if err := database.C.Save(&item).Error; err != nil {
publishedAt = lo.ToPtr(time.Now()) return item, err
} }
post = models.Post{ if item.GetReplyTo() != nil {
Content: content, go func() {
Attachments: attachments, var op models.Moment
Hashtags: tags, if err := database.C.Where("id = ?", item.GetReplyTo()).Preload("Author").First(&op).Error; err == nil {
Categories: categories, if op.Author.ID != item.GetAuthor().ID {
AuthorID: user.ID, postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID())
RealmID: realmId,
PublishedAt: *publishedAt,
RepostID: repostTo,
ReplyID: replyTo,
}
if err := database.C.Save(&post).Error; err != nil {
return post, err
}
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)
err := NotifyAccount( err := NotifyAccount(
op.Author, op.Author,
fmt.Sprintf("%s replied you", user.Name), fmt.Sprintf("%s replied you", item.GetAuthor().Name),
fmt.Sprintf("%s replied your post. Check it out!", user.Name), fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name),
&proto.NotifyLink{Label: "Related post", Url: postUrl}, &proto.NotifyLink{Label: "Related post", Url: postUrl},
) )
if err != nil { if err != nil {
@ -235,25 +275,23 @@ func NewPost(
} }
} }
} }
}()
} }
go func() {
var subscribers []models.AccountMembership var subscribers []models.AccountMembership
if err := database.C.Where(&models.AccountMembership{ if err := database.C.Where(&models.AccountMembership{
FollowingID: user.ID, FollowingID: item.GetAuthor().ID,
}).Preload("Follower").Find(&subscribers).Error; err != nil { }).Preload("Follower").Find(&subscribers).Error; err == nil && len(subscribers) > 0 {
return go func() {
}
accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account { accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account {
return item.Follower return item.Follower
}) })
for _, account := range accounts { for _, account := range accounts {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID) postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID())
err := NotifyAccount( err := NotifyAccount(
account, account,
fmt.Sprintf("%s just posted a post", user.Name), fmt.Sprintf("%s just posted a post", item.GetAuthor().Name),
"Account you followed post a brand new post. Check it out!", "Account you followed post a brand new post. Check it out!",
&proto.NotifyLink{Label: "Related post", Url: postUrl}, &proto.NotifyLink{Label: "Related post", Url: postUrl},
) )
@ -262,85 +300,52 @@ func NewPost(
} }
} }
}() }()
}
return post, nil return item, nil
} }
func EditPost( func (v *PostTypeContext[T]) Edit(item T) (T, error) {
post models.Post, item, err := v.MapCategoriesAndTags(item)
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 { if err != nil {
return post, err return item, err
}
}
for idx, tag := range tags {
tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
if err != nil {
return post, err
}
} }
if publishedAt == nil { err = database.C.Save(&item).Error
publishedAt = lo.ToPtr(time.Now())
}
post.Content = content return item, err
post.PublishedAt = *publishedAt
post.Hashtags = tags
post.Categories = categories
post.Attachments = attachments
err = database.C.Save(&post).Error
return post, err
} }
func LikePost(user models.Account, post models.Post) (bool, error) { func (v *PostTypeContext[T]) Delete(item T) error {
var like models.PostLike return database.C.Delete(&item).Error
if err := database.C.Where(&models.PostLike{ }
AccountID: user.ID,
PostID: post.ID, func (v *PostTypeContext[T]) ReactLike(user models.Account, id uint) (bool, error) {
}).First(&like).Error; err != nil { var count int64
if !errors.Is(err, gorm.ErrRecordNotFound) { table := viper.GetString("database.prefix") + v.GetTableName() + "_likes"
return true, err tx := database.C.Where("account_id = ?", user.ID).Where(v.GetTableName()+"id = ?", id)
} if tx.Count(&count); count <= 0 {
like = models.PostLike{ return true, database.C.Table(table).Create(map[string]any{
AccountID: user.ID, "AccountID": user.ID,
PostID: post.ID, v.TypeName + "ID": id,
} }).Error
return true, database.C.Save(&like).Error
} else { } 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) { func (v *PostTypeContext[T]) ReactDislike(user models.Account, id uint) (bool, error) {
var dislike models.PostDislike var count int64
if err := database.C.Where(&models.PostDislike{ table := viper.GetString("database.prefix") + v.GetTableName() + "_dislikes"
AccountID: user.ID, tx := database.C.Where("account_id = ?", user.ID).Where(v.GetTableName()+"id = ?", id)
PostID: post.ID, if tx.Count(&count); count <= 0 {
}).First(&dislike).Error; err != nil { return true, database.C.Table(table).Create(map[string]any{
if !errors.Is(err, gorm.ErrRecordNotFound) { "AccountID": user.ID,
return true, err v.TypeName + "ID": id,
} }).Error
dislike = models.PostDislike{
AccountID: user.ID,
PostID: post.ID,
}
return true, database.C.Save(&dislike).Error
} else { } 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
}