New datasets

This commit is contained in:
LittleSheep 2024-03-03 01:23:11 +08:00
parent e0bb05bee8
commit c23bde901b
19 changed files with 649 additions and 534 deletions

1
go.mod
View File

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

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/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=

View File

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

View File

@ -14,10 +14,15 @@ type Account struct {
Description string `json:"description"`
EmailAddress string `json:"email_address"`
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"`
LikedPosts []PostLike `json:"liked_posts"`
DislikedPosts []PostDislike `json:"disliked_posts"`
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"`

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"
)
type AttachmentType = uint8
const (
AttachmentOthers = AttachmentType(iota)
AttachmentPhoto
AttachmentVideo
AttachmentAudio
)
type Attachment struct {
BaseModel
@ -13,10 +22,12 @@ type Attachment struct {
Filesize int64 `json:"filesize"`
Filename string `json:"filename"`
Mimetype string `json:"mimetype"`
Type AttachmentType `json:"type"`
ExternalUrl string `json:"external_url"`
Post *Post `json:"post"`
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"`
}

View File

@ -6,7 +6,9 @@ type Tag struct {
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"`
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 {
@ -15,5 +17,7 @@ type Category struct {
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"`
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
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"`
Alias string `json:"alias" gorm:"uniqueIndex"`
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"`
PublishedAt *time.Time `json:"published_at"`
AuthorID uint `json:"author_id"`
Author Account `json:"author"`
@ -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)
}

View File

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

View File

@ -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"`

View File

@ -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())

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"
)
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())
}

View File

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

View File

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

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

@ -0,0 +1 @@
package services

View File

@ -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
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
LEFT JOIN (SELECT repost_id, COUNT(*) AS repost_count
FROM %sposts
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
WHERE t.id = ?`, prefix, prefix, prefix, prefix, prefix), post.ID).Scan(&reactInfo)
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,116 +197,77 @@ 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
}
func (v *PostTypeContext[T]) New(item T) (T, error) {
item, err := v.MapCategoriesAndTags(item)
if err != nil {
return item, err
}
var realmId *uint
if realm != nil {
if !realm.IsPublic {
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 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)
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", user.Name),
fmt.Sprintf("%s replied your post. Check it out!", user.Name),
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 {
@ -235,25 +275,23 @@ func NewPost(
}
}
}
}()
}
go func() {
var subscribers []models.AccountMembership
if err := database.C.Where(&models.AccountMembership{
FollowingID: user.ID,
}).Preload("Follower").Find(&subscribers).Error; err != nil {
return
}
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
})
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(
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!",
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
@ -262,85 +300,52 @@ func NewPost(
}
}
}()
}
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)
func (v *PostTypeContext[T]) Edit(item T) (T, error) {
item, err := v.MapCategoriesAndTags(item)
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
}
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
}