diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..c4444d9 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/hy_interactive + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index 27a1c12..ec2e0fa 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -5,23 +5,19 @@ import ( "gorm.io/gorm" ) -var DatabaseAutoActionRange = []any{ +var AutoMaintainRange = []any{ &models.Account{}, &models.Realm{}, &models.Category{}, &models.Tag{}, - &models.Moment{}, - &models.Article{}, - &models.Comment{}, + &models.Post{}, &models.Reaction{}, &models.Attachment{}, } func RunMigration(source *gorm.DB) error { if err := source.AutoMigrate( - append([]any{ - &models.AccountMembership{}, - }, DatabaseAutoActionRange...)..., + AutoMaintainRange..., ); err != nil { return err } diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index 73eddd0..aac9edb 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -1,7 +1,5 @@ package models -import "time" - // Account profiles basically fetched from Hydrogen.Passport // But cache at here for better usage // At the same time this model can make relations between local models @@ -15,19 +13,8 @@ type Account struct { 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"` + Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"` Reactions []Reaction `json:"reactions"` ExternalID uint `json:"external_id"` } - -type AccountMembership struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Follower Account `json:"follower"` - Following Account `json:"following"` - FollowerID uint - FollowingID uint -} diff --git a/pkg/models/articles.go b/pkg/models/articles.go deleted file mode 100644 index 1ebabfa..0000000 --- a/pkg/models/articles.go +++ /dev/null @@ -1,41 +0,0 @@ -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"` - Reactions []Reaction `json:"reactions"` - Attachments []Attachment `json:"attachments"` - 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 e2e3e67..a6a0fdc 100644 --- a/pkg/models/attachments.go +++ b/pkg/models/attachments.go @@ -19,18 +19,16 @@ const ( type Attachment struct { BaseModel - FileID string `json:"file_id"` - Filesize int64 `json:"filesize"` - Filename string `json:"filename"` - Mimetype string `json:"mimetype"` - Hashcode string `json:"hashcode"` - 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"` + FileID string `json:"file_id"` + Filesize int64 `json:"filesize"` + Filename string `json:"filename"` + Mimetype string `json:"mimetype"` + Hashcode string `json:"hashcode"` + Type AttachmentType `json:"type"` + Author Account `json:"author"` + AuthorID uint `json:"author_id"` + + PostID *uint `json:"post_id"` } func (v Attachment) GetStoragePath() string { diff --git a/pkg/models/categories.go b/pkg/models/categories.go index 4048e4e..6a52e67 100644 --- a/pkg/models/categories.go +++ b/pkg/models/categories.go @@ -3,21 +3,17 @@ 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"` - Articles []Article `json:"articles" gorm:"many2many:article_tags"` - Moments []Moment `json:"moments" gorm:"many2many:moment_tags"` - Comments []Comment `json:"comments" gorm:"many2many:comment_tags"` + 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"` } 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"` - Articles []Article `json:"articles" gorm:"many2many:article_categories"` - Moments []Moment `json:"moments" gorm:"many2many:moment_categories"` - Comments []Comment `json:"comments" gorm:"many2many:comment_categories"` + 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_categories"` } diff --git a/pkg/models/comments.go b/pkg/models/comments.go deleted file mode 100644 index ad2ef33..0000000 --- a/pkg/models/comments.go +++ /dev/null @@ -1,38 +0,0 @@ -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"` - Reactions []Reaction `json:"reactions"` - Attachments []Attachment `json:"attachments"` - 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/feed.go b/pkg/models/feed.go deleted file mode 100644 index f1b22db..0000000 --- a/pkg/models/feed.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -type Feed struct { - BaseModel - - Alias string `json:"alias"` - Title string `json:"title"` - Description string `json:"description"` - Content string `json:"content"` - ModelType string `json:"model_type"` - - CommentCount int64 `json:"comment_count"` - ReactionCount int64 `json:"reaction_count"` - - AuthorID uint `json:"author_id"` - RealmID *uint `json:"realm_id"` - - Author Account `json:"author" gorm:"embedded"` - - Attachments []Attachment `json:"attachments" gorm:"-"` - ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` -} diff --git a/pkg/models/moments.go b/pkg/models/moments.go deleted file mode 100644 index f25b3c0..0000000 --- a/pkg/models/moments.go +++ /dev/null @@ -1,41 +0,0 @@ -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"` - Reactions []Reaction `json:"reactions"` - Attachments []Attachment `json:"attachments"` - 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 94d1cef..43949b1 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -12,53 +12,30 @@ type PostReactInfo struct { RepostCount int64 `json:"repost_count"` } -type PostBase struct { +type Post struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` + Alias string `json:"alias" gorm:"uniqueIndex"` + Content string `json:"content"` + Tags []Tag `json:"tags" gorm:"many2many:post_tags"` + Categories []Category `json:"categories" gorm:"many2many:post_categories"` + Reactions []Reaction `json:"reactions"` + Attachments []Attachment `json:"attachments"` + Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"` + ReplyID *uint `json:"reply_id"` + RepostID *uint `json:"repost_id"` + RealmID *uint `json:"realm_id"` + ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"` + RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"` + Realm *Realm `json:"realm"` + PublishedAt *time.Time `json:"published_at"` AuthorID uint `json:"author_id"` Author Account `json:"author"` // Dynamic Calculated Values - ReactionList map[string]int64 `json:"reaction_list" 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) SetReactionList(list map[string]int64) { - p.ReactionList = list -} - -type PostInterface interface { - GetID() uint - GetHashtags() []Tag - GetCategories() []Category - GetReplyTo() PostInterface - GetRepostTo() PostInterface - GetAuthor() Account - GetRealm() *Realm - - SetHashtags([]Tag) - SetCategories([]Category) - SetReactionList(map[string]int64) + ReplyCount int64 `json:"comment_count"` + ReactionCount int64 `json:"reaction_count"` + ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` } diff --git a/pkg/models/reactions.go b/pkg/models/reactions.go index 0ffd22d..da1762a 100644 --- a/pkg/models/reactions.go +++ b/pkg/models/reactions.go @@ -20,8 +20,6 @@ type Reaction struct { Symbol string `json:"symbol"` Attitude ReactionAttitude `json:"attitude"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` - CommentID *uint `json:"comment_id"` + PostID *uint `json:"post_id"` AccountID uint `json:"account_id"` } diff --git a/pkg/models/realms.go b/pkg/models/realms.go index 629d65a..e40afac 100644 --- a/pkg/models/realms.go +++ b/pkg/models/realms.go @@ -5,12 +5,11 @@ package models type Realm struct { BaseModel - Alias string `json:"alias"` - Name string `json:"name"` - Description string `json:"description"` - Articles []Article `json:"article"` - Moments []Moment `json:"moments"` - IsPublic bool `json:"is_public"` - IsCommunity bool `json:"is_community"` - ExternalID uint `json:"external_id"` + Alias string `json:"alias"` + Name string `json:"name"` + Description string `json:"description"` + Posts []Post `json:"posts"` + IsPublic bool `json:"is_public"` + IsCommunity bool `json:"is_community"` + ExternalID uint `json:"external_id"` } diff --git a/pkg/server/articles_api.go b/pkg/server/articles_api.go deleted file mode 100644 index 225b760..0000000 --- a/pkg/server/articles_api.go +++ /dev/null @@ -1,141 +0,0 @@ -package server - -import ( - "fmt" - "strings" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/database" - "git.solsynth.dev/hydrogen/interactive/pkg/models" - "git.solsynth.dev/hydrogen/interactive/pkg/services" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -func contextArticle() *services.PostTypeContext { - return &services.PostTypeContext{ - Tx: database.C, - TableName: "articles", - ColumnName: "article", - CanReply: false, - CanRepost: false, - } -} - -func createArticle(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - - var data struct { - Alias string `json:"alias" form:"alias"` - Title string `json:"title" form:"title" validate:"required"` - Description string `json:"description" form:"description"` - Content string `json:"content" form:"content" validate:"required"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - RealmAlias string `json:"realm" form:"realm"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Alias) == 0 { - data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") - } - - item := &models.Article{ - PostBase: models.PostBase{ - Alias: data.Alias, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - }, - Hashtags: data.Hashtags, - Categories: data.Categories, - Attachments: data.Attachments, - Title: data.Title, - Description: data.Description, - Content: data.Content, - } - - if len(data.RealmAlias) > 0 { - if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err)) - } else { - item.RealmID = &realm.ID - } - } - - if item, err := services.NewPost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - -func editArticle(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("articleId", 0) - - var data struct { - Alias string `json:"alias" form:"alias" validate:"required"` - Title string `json:"title" form:"title" validate:"required"` - Description string `json:"description" form:"description"` - Content string `json:"content" form:"content" validate:"required"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } - - var item *models.Article - if err := database.C.Where(models.Article{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - item.Alias = data.Alias - item.Title = data.Title - item.Description = data.Description - item.Content = data.Content - item.PublishedAt = data.PublishedAt - item.Hashtags = data.Hashtags - item.Categories = data.Categories - item.Attachments = data.Attachments - - if item, err := services.EditPost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - -func deleteArticle(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("articleId", 0) - - var item *models.Article - if err := database.C.Where(models.Article{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - if err := services.DeletePost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.SendStatus(fiber.StatusOK) -} diff --git a/pkg/server/comments_api.go b/pkg/server/comments_api.go deleted file mode 100644 index 7b0798f..0000000 --- a/pkg/server/comments_api.go +++ /dev/null @@ -1,196 +0,0 @@ -package server - -import ( - "fmt" - "github.com/spf13/viper" - "strings" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/database" - "git.solsynth.dev/hydrogen/interactive/pkg/models" - "git.solsynth.dev/hydrogen/interactive/pkg/services" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -func contextComment() *services.PostTypeContext { - return &services.PostTypeContext{ - Tx: database.C, - TableName: "comments", - ColumnName: "comment", - CanReply: false, - CanRepost: true, - } -} - -func listComment(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - - alias := c.Params("postId") - - mx := c.Locals(postContextKey).(*services.PostTypeContext). - FilterPublishedAt(time.Now()) - - item, err := mx.GetViaAlias(alias) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - data, err := mx.ListComment(item.ID, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - count, err := mx.CountComment(item.ID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - return c.JSON(fiber.Map{ - "count": count, - "data": data, - }) -} - -func createComment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - - var data struct { - Content string `json:"content" form:"content" validate:"required"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - ReplyTo uint `json:"reply_to" form:"reply_to"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } - - item := &models.Comment{ - PostBase: models.PostBase{ - Alias: strings.ReplaceAll(uuid.NewString(), "-", ""), - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - }, - Hashtags: data.Hashtags, - Categories: data.Categories, - Attachments: data.Attachments, - Content: data.Content, - } - - postType := c.Params("postType") - alias := c.Params("postId") - - var err error - var res models.Feed - - var columnName string - var tableName string - - switch postType { - case "moments": - columnName = "moment" - tableName = viper.GetString("database.table_prefix") + "moments" - err = database.C.Model(&models.Moment{}).Where("alias = ?", alias).Select("id").First(&res).Error - case "articles": - columnName = "article" - tableName = viper.GetString("database.table_prefix") + "articles" - err = database.C.Model(&models.Article{}).Where("alias = ?", alias).Select("id").First(&res).Error - default: - return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource") - } - - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) - } else { - switch postType { - case "moments": - item.MomentID = &res.ID - case "articles": - item.ArticleID = &res.ID - } - } - - var relatedCount int64 - if data.ReplyTo > 0 { - if err := database.C.Where("id = ?", data.ReplyTo). - Model(&models.Comment{}).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 { - item.ReplyID = &data.ReplyTo - } - } - - item, err = services.NewPost(item) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - // Notify the original poster their post is commented by someone - go services.CommentNotify(item, res, columnName, tableName) - - return c.JSON(item) -} - -func editComment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("commentId", 0) - - var data struct { - Content string `json:"content" form:"content" validate:"required"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } - - var item *models.Comment - if err := database.C.Where(models.Comment{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - item.Content = data.Content - item.PublishedAt = data.PublishedAt - item.Hashtags = data.Hashtags - item.Categories = data.Categories - - if item, err := services.EditPost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - -func deleteComment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("commentId", 0) - - var item *models.Comment - if err := database.C.Where(models.Comment{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - if err := services.DeletePost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.SendStatus(fiber.StatusOK) -} diff --git a/pkg/server/feed_api.go b/pkg/server/feed_api.go index c43d9dc..b457d52 100644 --- a/pkg/server/feed_api.go +++ b/pkg/server/feed_api.go @@ -1,200 +1,49 @@ package server import ( - "fmt" - "git.solsynth.dev/hydrogen/interactive/pkg/services" - "strings" - "git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/models" + "git.solsynth.dev/hydrogen/interactive/pkg/services" "github.com/gofiber/fiber/v2" - "github.com/samber/lo" - "github.com/spf13/viper" -) - -const ( - queryArticle = "id, created_at, updated_at, alias, title, NULL as content, description, realm_id, author_id, 'article' as model_type" - queryMoment = "id, created_at, updated_at, alias, NULL as title, content, NULL as description, realm_id, author_id, 'moment' as model_type" ) func listFeed(c *fiber.Ctx) error { take := c.QueryInt("take", 0) offset := c.QueryInt("offset", 0) - realmAlias := c.Query("realm") + realmId := c.QueryInt("realmId", 0) - if take > 20 { - take = 20 + tx := database.C + if realmId > 0 { + tx = services.FilterWithRealm(tx, uint(realmId)) } - var whereConditions []string - - if len(realmAlias) > 0 { - realm, err := services.GetRealmWithAlias(realmAlias) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("related realm was not found: %v", err)) - } - whereConditions = append(whereConditions, fmt.Sprintf("feed.realm_id = %d", realm.ID)) - } - - var author models.Account if len(c.Query("authorId")) > 0 { + var author models.Account if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) - } else { - whereConditions = append(whereConditions, fmt.Sprintf("feed.author_id = %d", author.ID)) } + tx = tx.Where("author_id = ?", author.ID) } - var whereStatement string - if len(whereConditions) > 0 { - whereStatement += "WHERE " + strings.Join(whereConditions, " AND ") + 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 result []*models.Feed - - userTable := viper.GetString("database.prefix") + "accounts" - commentTable := viper.GetString("database.prefix") + "comments" - reactionTable := viper.GetString("database.prefix") + "reactions" - - database.C.Raw( - fmt.Sprintf(`SELECT feed.*, author.*, - COALESCE(comment_count, 0) AS comment_count, - COALESCE(reaction_count, 0) AS reaction_count - FROM (? UNION ALL ?) AS feed - INNER JOIN %s AS author ON author_id = author.id - LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count - FROM %s - GROUP BY article_id, moment_id) AS comments - ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR - (feed.model_type = 'moment' AND feed.id = comments.moment_id) - LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS reaction_count - FROM %s - GROUP BY article_id, moment_id) AS reactions - ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR - (feed.model_type = 'moment' AND feed.id = reactions.moment_id) - %s ORDER BY feed.created_at desc LIMIT ? OFFSET ?`, - userTable, - commentTable, - reactionTable, - whereStatement, - ), - database.C.Select(queryArticle).Model(&models.Article{}), - database.C.Select(queryMoment).Model(&models.Moment{}), - take, - offset, - ).Scan(&result) - - if !c.QueryBool("noReact", false) { - var reactions []struct { - PostID uint - Symbol string - Count int64 - } - - revertReaction := func(dataset string) error { - itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) { - return item, item.ModelType == dataset - }), func(item *models.Feed) (uint, *models.Feed) { - return item.ID, item - }) - - idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool { - return item.ModelType == dataset - }), func(item *models.Feed, index int) uint { - return item.ID - }) - - if err := database.C.Model(&models.Reaction{}). - Select(dataset+"_id as post_id, symbol, COUNT(id) as count"). - Where(dataset+"_id IN (?)", idx). - Group("post_id, symbol"). - Scan(&reactions).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - list := map[uint]map[string]int64{} - for _, info := range reactions { - if _, ok := list[info.PostID]; !ok { - list[info.PostID] = make(map[string]int64) - } - list[info.PostID][info.Symbol] = info.Count - } - - for k, v := range list { - if post, ok := itemMap[k]; ok { - post.ReactionList = v - } - } - - return nil - } - - if err := revertReaction("article"); err != nil { - return err - } - if err := revertReaction("moment"); err != nil { - return err - } + count, err := services.CountPost(tx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - if !c.QueryBool("noAttachment", false) { - revertAttachment := func(dataset string) error { - var attachments []struct { - models.Attachment - - PostID uint `json:"post_id"` - } - - itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) { - return item, item.ModelType == dataset - }), func(item *models.Feed) (uint, *models.Feed) { - return item.ID, item - }) - - idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool { - return item.ModelType == dataset - }), func(item *models.Feed, index int) uint { - return item.ID - }) - - if err := database.C. - Model(&models.Attachment{}). - Select(dataset+"_id as post_id, *"). - Where(dataset+"_id IN (?)", idx). - Scan(&attachments).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - list := map[uint][]models.Attachment{} - for _, info := range attachments { - list[info.PostID] = append(list[info.PostID], info.Attachment) - } - - for k, v := range list { - if post, ok := itemMap[k]; ok { - post.Attachments = v - } - } - - return nil - } - - if err := revertAttachment("article"); err != nil { - return err - } - if err := revertAttachment("moment"); err != nil { - return err - } + items, err := services.ListPost(tx, take, offset) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - var count int64 - database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`, - database.C.Select(queryArticle).Model(&models.Article{}), - database.C.Select(queryMoment).Model(&models.Moment{}), - ).Scan(&count) - return c.JSON(fiber.Map{ "count": count, - "data": result, + "data": items, }) } diff --git a/pkg/server/moments_api.go b/pkg/server/moments_api.go deleted file mode 100644 index 57f5287..0000000 --- a/pkg/server/moments_api.go +++ /dev/null @@ -1,147 +0,0 @@ -package server - -import ( - "fmt" - "strings" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/database" - "git.solsynth.dev/hydrogen/interactive/pkg/models" - "git.solsynth.dev/hydrogen/interactive/pkg/services" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -func contextMoment() *services.PostTypeContext { - return &services.PostTypeContext{ - Tx: database.C, - TableName: "moments", - ColumnName: "moment", - CanReply: false, - CanRepost: true, - } -} - -func createMoment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - - var data struct { - Alias string `json:"alias" form:"alias"` - Content string `json:"content" form:"content" validate:"required,max=1024"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - RealmAlias string `json:"realm" form:"realm"` - RepostTo uint `json:"repost_to" form:"repost_to"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Alias) == 0 { - data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") - } - - item := &models.Moment{ - PostBase: models.PostBase{ - Alias: data.Alias, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - }, - Hashtags: data.Hashtags, - Categories: data.Categories, - Attachments: data.Attachments, - Content: data.Content, - } - - var relatedCount int64 - if data.RepostTo > 0 { - 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 { - item.RepostID = &data.RepostTo - } - } - - if len(data.RealmAlias) > 0 { - if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err)) - } else { - item.RealmID = &realm.ID - } - } - - item, err := services.NewPost(item) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(item) -} - -func editMoment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("momentId", 0) - - var data struct { - Alias string `json:"alias" form:"alias" validate:"required"` - Content string `json:"content" form:"content" validate:"required,max=1024"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - } - - if err := BindAndValidate(c, &data); err != nil { - return err - } - - var item *models.Moment - if err := database.C.Where(models.Moment{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - item.Alias = data.Alias - item.Content = data.Content - item.PublishedAt = data.PublishedAt - item.Hashtags = data.Hashtags - item.Categories = data.Categories - item.Attachments = data.Attachments - - if item, err := services.EditPost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - -func deleteMoment(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("momentId", 0) - - var item *models.Moment - if err := database.C.Where(models.Moment{ - PostBase: models.PostBase{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - if err := services.DeletePost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.SendStatus(fiber.StatusOK) -} diff --git a/pkg/server/posts_api.go b/pkg/server/posts_api.go index fadaff9..7911c0e 100644 --- a/pkg/server/posts_api.go +++ b/pkg/server/posts_api.go @@ -2,6 +2,8 @@ package server import ( "fmt" + "github.com/google/uuid" + "strings" "time" "git.solsynth.dev/hydrogen/interactive/pkg/database" @@ -11,38 +13,17 @@ import ( "github.com/samber/lo" ) -var postContextKey = "ptx" - -func useDynamicContext(c *fiber.Ctx) error { - postType := c.Params("postType") - switch postType { - case "articles": - c.Locals(postContextKey, contextArticle()) - case "moments": - c.Locals(postContextKey, contextMoment()) - case "comments": - c.Locals(postContextKey, contextComment()) - default: - return fiber.NewError(fiber.StatusBadRequest, "invalid dataset") - } - - return c.Next() -} - func getPost(c *fiber.Ctx) error { alias := c.Params("postId") - mx := c.Locals(postContextKey).(*services.PostTypeContext). - FilterPublishedAt(time.Now()) - - item, err := mx.GetViaAlias(alias) + item, err := services.GetPostWithAlias(alias) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - item.CommentCount = mx.CountComments(item.ID) - item.ReactionCount = mx.CountReactions(item.ID) - item.ReactionList, err = mx.ListReactions(item.ID) + item.ReplyCount = services.CountPostReply(item.ID) + item.ReactionCount = services.CountPostReactions(item.ID) + item.ReactionList, err = services.ListPostReactions(item.ID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -55,36 +36,32 @@ func listPost(c *fiber.Ctx) error { offset := c.QueryInt("offset", 0) realmId := c.QueryInt("realmId", 0) - mx := c.Locals(postContextKey).(*services.PostTypeContext). - FilterPublishedAt(time.Now()). - FilterRealm(uint(realmId)). - SortCreatedAt("desc") + tx := database.C + if realmId > 0 { + tx = services.FilterWithRealm(tx, uint(realmId)) + } - var author models.Account if len(c.Query("authorId")) > 0 { + var author models.Account if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - mx = mx.FilterAuthor(author.ID) + tx = tx.Where("author_id = ?", author.ID) } if len(c.Query("category")) > 0 { - mx = mx.FilterWithCategory(c.Query("category")) + tx = services.FilterPostWithCategory(tx, c.Query("category")) } if len(c.Query("tag")) > 0 { - mx = mx.FilterWithTag(c.Query("tag")) + tx = services.FilterPostWithTag(tx, c.Query("tag")) } - if !c.QueryBool("reply", true) { - mx = mx.FilterReply(true) - } - - count, err := mx.Count() + count, err := services.CountPost(tx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := mx.List(take, offset) + items, err := services.ListPost(tx, take, offset) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -95,6 +72,124 @@ func listPost(c *fiber.Ctx) error { }) } +func createPost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + + var data struct { + Alias string `json:"alias" form:"alias"` + Content string `json:"content" form:"content" validate:"required,max=4096"` + Tags []models.Tag `json:"tags" form:"tags"` + Categories []models.Category `json:"categories" form:"categories"` + Attachments []models.Attachment `json:"attachments" form:"attachments"` + PublishedAt *time.Time `json:"published_at" form:"published_at"` + RealmAlias string `json:"realm" form:"realm"` + RepostTo uint `json:"repost_to" form:"repost_to"` + } + + if err := BindAndValidate(c, &data); err != nil { + return err + } else if len(data.Alias) == 0 { + data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") + } + + item := models.Post{ + Alias: data.Alias, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + Tags: data.Tags, + Categories: data.Categories, + Attachments: data.Attachments, + Content: data.Content, + } + + var relatedCount int64 + if data.RepostTo > 0 { + if err := database.C.Where("id = ?", data.RepostTo). + 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 { + item.RepostID = &data.RepostTo + } + } + + if len(data.RealmAlias) > 0 { + if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err)) + } else { + item.RealmID = &realm.ID + } + } + + item, err := services.NewPost(user, item) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(item) +} + +func editPost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + id, _ := c.ParamsInt("postId", 0) + + var data struct { + Alias string `json:"alias" form:"alias" validate:"required"` + Content string `json:"content" form:"content" validate:"required,max=1024"` + PublishedAt *time.Time `json:"published_at" form:"published_at"` + Tags []models.Tag `json:"tags" form:"tags"` + Categories []models.Category `json:"categories" form:"categories"` + Attachments []models.Attachment `json:"attachments" form:"attachments"` + } + + if err := BindAndValidate(c, &data); err != nil { + return err + } + + var item models.Post + if err := database.C.Where(models.Post{ + BaseModel: models.BaseModel{ID: uint(id)}, + AuthorID: user.ID, + }).First(&item).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + item.Alias = data.Alias + item.Content = data.Content + item.PublishedAt = data.PublishedAt + item.Tags = data.Tags + item.Categories = data.Categories + item.Attachments = data.Attachments + + if item, err := services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) + } +} + +func deletePost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + id, _ := c.ParamsInt("postId", 0) + + var item models.Post + if err := database.C.Where(models.Post{ + BaseModel: models.BaseModel{ID: uint(id)}, + AuthorID: user.ID, + }).First(&item).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + if err := services.DeletePost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.SendStatus(fiber.StatusOK) +} + func reactPost(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) @@ -107,45 +202,23 @@ func reactPost(c *fiber.Ctx) error { return err } - mx := c.Locals(postContextKey).(*services.PostTypeContext) - reaction := models.Reaction{ Symbol: data.Symbol, Attitude: data.Attitude, AccountID: user.ID, } - postType := c.Params("postType") alias := c.Params("postId") - var err error - var res models.Feed + var res models.Post - switch postType { - case "moments": - err = database.C.Model(&models.Moment{}).Where("id = ?", alias).Select("id").First(&res).Error - case "articles": - err = database.C.Model(&models.Article{}).Where("id = ?", alias).Select("id").First(&res).Error - case "comments": - err = database.C.Model(&models.Comment{}).Where("id = ?", alias).Select("id").First(&res).Error - default: - return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource") - } - - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) + if err := database.C.Where("id = ?", alias).Select("id").First(&res).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post to react: %v", err)) } else { - switch postType { - case "moments": - reaction.MomentID = &res.ID - case "articles": - reaction.ArticleID = &res.ID - case "comments": - reaction.CommentID = &res.ID - } + reaction.PostID = &res.ID } - if positive, reaction, err := mx.React(reaction); err != nil { + if positive, reaction, err := services.ReactPost(reaction); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } else { return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction) diff --git a/pkg/server/replies_api.go b/pkg/server/replies_api.go new file mode 100644 index 0000000..fc20278 --- /dev/null +++ b/pkg/server/replies_api.go @@ -0,0 +1,52 @@ +package server + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/database" + "git.solsynth.dev/hydrogen/interactive/pkg/models" + "git.solsynth.dev/hydrogen/interactive/pkg/services" + "github.com/gofiber/fiber/v2" +) + +func listReplies(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + + tx := database.C + var post models.Post + if err := database.C.Where("alias = ?", c.Params("postId")).First(&post).Error; err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post: %v", err)) + } else { + tx = services.FilterPostReply(tx, post.ID) + } + + if len(c.Query("authorId")) > 0 { + var author models.Account + 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("author_id = ?", author.ID) + } + + 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")) + } + + count, err := services.CountPost(tx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + items, err := services.ListPost(tx, take, offset) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": items, + }) +} diff --git a/pkg/server/startup.go b/pkg/server/startup.go index d9c3ad2..28a1028 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -62,8 +62,6 @@ func NewServer() { { api.Get("/users/me", authMiddleware, getUserinfo) api.Get("/users/:accountId", getOthersInfo) - api.Get("/users/:accountId/follow", authMiddleware, getAccountFollowed) - api.Post("/users/:accountId/follow", authMiddleware, doFollowAccount) api.Get("/attachments/o/:fileId", readAttachment) api.Post("/attachments", authMiddleware, uploadAttachment) @@ -71,33 +69,16 @@ func NewServer() { api.Get("/feed", listFeed) - posts := api.Group("/p/:postType").Use(useDynamicContext).Name("Dataset Universal API") + posts := api.Group("/posts").Name("Posts API") { posts.Get("/", listPost) posts.Get("/:postId", getPost) + posts.Post("/", authMiddleware, createPost) posts.Post("/:postId/react", authMiddleware, reactPost) - posts.Get("/:postId/comments", listComment) - posts.Post("/:postId/comments", authMiddleware, createComment) - } + posts.Put("/:postId", authMiddleware, editPost) + posts.Delete("/:postId", authMiddleware, deletePost) - moments := api.Group("/p/moments").Name("Moments API") - { - moments.Post("/", authMiddleware, createMoment) - moments.Put("/:momentId", authMiddleware, editMoment) - moments.Delete("/:momentId", authMiddleware, deleteMoment) - } - - articles := api.Group("/p/articles").Name("Articles API") - { - articles.Post("/", authMiddleware, createArticle) - articles.Put("/:articleId", authMiddleware, editArticle) - articles.Delete("/:articleId", authMiddleware, deleteArticle) - } - - comments := api.Group("/p/comments").Name("Comments API") - { - comments.Put("/:commentId", authMiddleware, editComment) - comments.Delete("/:commentId", authMiddleware, deleteComment) + posts.Get("/:postId/replies", listReplies) } api.Get("/categories", listCategories) diff --git a/pkg/server/users_api.go b/pkg/server/users_api.go index d62ff55..89a237b 100644 --- a/pkg/server/users_api.go +++ b/pkg/server/users_api.go @@ -3,7 +3,6 @@ package server import ( "git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/models" - "git.solsynth.dev/hydrogen/interactive/pkg/services" "github.com/gofiber/fiber/v2" ) @@ -32,45 +31,3 @@ func getOthersInfo(c *fiber.Ctx) error { return c.JSON(data) } - -func getAccountFollowed(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - accountId, _ := c.ParamsInt("accountId", 0) - - var data models.Account - if err := database.C. - Where(&models.Account{BaseModel: models.BaseModel{ID: uint(accountId)}}). - First(&data).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - _, status := services.GetAccountFollowed(user, data) - - return c.JSON(fiber.Map{ - "is_followed": status, - }) -} - -func doFollowAccount(c *fiber.Ctx) error { - user := c.Locals("principal").(models.Account) - id, _ := c.ParamsInt("accountId", 0) - - var account models.Account - if err := database.C.Where(&models.Account{ - BaseModel: models.BaseModel{ID: uint(id)}, - }).First(&account).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - if _, ok := services.GetAccountFollowed(user, account); ok { - if err := services.UnfollowAccount(user.ID, account.ID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - return c.SendStatus(fiber.StatusNoContent) - } else { - if err := services.FollowAccount(user.ID, account.ID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - return c.SendStatus(fiber.StatusCreated) - } -} diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index 862f062..5b70cea 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -11,30 +11,6 @@ import ( "time" ) -func FollowAccount(followerId, followingId uint) error { - relationship := models.AccountMembership{ - FollowerID: followerId, - FollowingID: followingId, - } - return database.C.Create(&relationship).Error -} - -func UnfollowAccount(followerId, followingId uint) error { - return database.C.Where(models.AccountMembership{ - FollowerID: followerId, - FollowingID: followingId, - }).Delete(&models.AccountMembership{}).Error -} - -func GetAccountFollowed(user models.Account, target models.Account) (models.AccountMembership, bool) { - var relationship models.AccountMembership - err := database.C.Model(&models.AccountMembership{}). - Where(&models.AccountMembership{FollowerID: user.ID, FollowingID: target.ID}). - First(&relationship). - Error - return relationship, err == nil -} - func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) { var user models.Account if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil { diff --git a/pkg/services/cleaner.go b/pkg/services/cleaner.go index 1b8c935..34cb5be 100644 --- a/pkg/services/cleaner.go +++ b/pkg/services/cleaner.go @@ -11,7 +11,7 @@ func DoAutoDatabaseCleanup() { log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...") var count int64 - for _, model := range database.DatabaseAutoActionRange { + for _, model := range database.AutoMaintainRange { tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline) if tx.Error != nil { log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...") diff --git a/pkg/services/comments.go b/pkg/services/comments.go deleted file mode 100644 index 9c30488..0000000 --- a/pkg/services/comments.go +++ /dev/null @@ -1,107 +0,0 @@ -package services - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" - "github.com/rs/zerolog/log" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/database" - "git.solsynth.dev/hydrogen/interactive/pkg/models" - "github.com/samber/lo" - "github.com/spf13/viper" -) - -func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...bool) ([]*models.Feed, error) { - if take > 20 { - take = 20 - } - - var items []*models.Feed - table := viper.GetString("database.prefix") + "comments" - userTable := viper.GetString("database.prefix") + "accounts" - if err := v.Tx. - Table(table). - Select("*, ? as model_type", "comment"). - Where(v.ColumnName+"_id = ?", id). - Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)). - Limit(take).Offset(offset).Find(&items).Error; err != nil { - return items, err - } - - idx := lo.Map(items, func(item *models.Feed, index int) uint { - return item.ID - }) - - if len(noReact) <= 0 || !noReact[0] { - var reactions []struct { - PostID uint - Symbol string - Count int64 - } - - if err := database.C.Model(&models.Reaction{}). - Select("comment_id as post_id, symbol, COUNT(id) as count"). - Where("comment_id IN (?)", idx). - Group("post_id, symbol"). - Scan(&reactions).Error; err != nil { - return items, err - } - - itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { - return item.ID, item - }) - - list := map[uint]map[string]int64{} - for _, info := range reactions { - if _, ok := list[info.PostID]; !ok { - list[info.PostID] = make(map[string]int64) - } - list[info.PostID][info.Symbol] = info.Count - } - - for k, v := range list { - if post, ok := itemMap[k]; ok { - post.ReactionList = v - } - } - } - - return items, nil -} - -func (v *PostTypeContext) CountComment(id uint) (int64, error) { - var count int64 - if err := database.C. - Model(&models.Comment{}). - Where(v.ColumnName+"_id = ?", id). - Where("published_at <= ?", time.Now()). - Count(&count).Error; err != nil { - return count, err - } - - return count, nil -} - -func CommentNotify(this models.PostInterface, original models.Feed, columnName, tableName string) { - var op models.Feed - if err := database.C. - Where(columnName+"_id = ?", original.ID). - Preload("Author"). - Table(tableName). - First(&op).Error; err == nil { - if op.Author.ID != this.GetAuthor().ID { - postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), this.GetID()) - err := NotifyAccount( - op.Author, - fmt.Sprintf("%s commented you", this.GetAuthor().Name), - fmt.Sprintf("%s commented your post. Check it out!", this.GetAuthor().Name), - false, - &proto.NotifyLink{Label: "Related post", Url: postUrl}, - ) - if err != nil { - log.Error().Err(err).Msg("An error occurred when notifying user...") - } - } - } -} diff --git a/pkg/services/moments.go b/pkg/services/moments.go deleted file mode 100644 index 5e568ea..0000000 --- a/pkg/services/moments.go +++ /dev/null @@ -1 +0,0 @@ -package services diff --git a/pkg/services/posts.go b/pkg/services/posts.go index 257899f..5c4a832 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -3,8 +3,6 @@ package services import ( "errors" "fmt" - "time" - "git.solsynth.dev/hydrogen/interactive/pkg/database" "git.solsynth.dev/hydrogen/interactive/pkg/models" "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" @@ -12,131 +10,90 @@ import ( "github.com/samber/lo" "github.com/spf13/viper" "gorm.io/gorm" + "time" ) -type PostTypeContext struct { - Tx *gorm.DB - - TableName string - ColumnName string - CanReply bool - CanRepost bool +func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB { + return tx.Joins("JOIN post_categories ON posts.id = post_categories.post_id"). + Joins("JOIN post_categories ON post_categories.id = post_categories.category_id"). + Where("post_categories.alias = ?", alias) } -func (v *PostTypeContext) FilterWithCategory(alias string) *PostTypeContext { - name := v.ColumnName - v.Tx.Joins(fmt.Sprintf("JOIN %s_categories ON %s.id = %s_categories.%s_id", name, v.TableName, name, name)). - Joins(fmt.Sprintf("JOIN %s_categories ON %s_categories.id = %s_categories.category_id", name, name, name)). - Where(name+"_categories.alias = ?", alias) - return v +func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB { + return tx.Joins("JOIN post_tags ON posts.id = post_tags.post_id"). + Joins("JOIN post_tags ON post_tags.id = post_tags.category_id"). + Where("post_tags.alias = ?", alias) } -func (v *PostTypeContext) FilterWithTag(alias string) *PostTypeContext { - name := v.ColumnName - v.Tx.Joins(fmt.Sprintf("JOIN %s_tags ON %s.id = %s_tags.%s_id", name, v.TableName, name, name)). - Joins(fmt.Sprintf("JOIN %s_tags ON %s_tags.id = %s_tags.category_id", name, name, name)). - Where(name+"_tags.alias = ?", alias) - return v -} - -func (v *PostTypeContext) FilterPublishedAt(date time.Time) *PostTypeContext { - v.Tx.Where("published_at <= ? AND published_at IS NULL", date) - return v -} - -func (v *PostTypeContext) FilterRealm(id uint) *PostTypeContext { +func FilterWithRealm(tx *gorm.DB, id uint) *gorm.DB { if id > 0 { - v.Tx = v.Tx.Where("realm_id = ?", id) + return tx.Where("realm_id = ?", id) } else { - v.Tx = v.Tx.Where("realm_id IS NULL") + return tx.Where("realm_id IS NULL") } - return v } -func (v *PostTypeContext) FilterAuthor(id uint) *PostTypeContext { - v.Tx = v.Tx.Where("author_id = ?", id) - return v -} - -func (v *PostTypeContext) FilterReply(condition bool) *PostTypeContext { - if condition { - v.Tx = v.Tx.Where("reply_id IS NOT NULL") +func FilterPostReply(tx *gorm.DB, replyTo ...uint) *gorm.DB { + if len(replyTo) > 0 && replyTo[0] > 0 { + return tx.Where("reply_id = ?", replyTo[0]) } else { - v.Tx = v.Tx.Where("reply_id IS NULL") + return tx.Where("reply_id IS NULL") } - return v } -func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext { - v.Tx.Order(fmt.Sprintf("created_at %s", order)) - return v +func FilterPostWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB { + return tx.Where("published_at <= ? AND published_at IS NULL", date) } -func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { - var item models.Feed - table := viper.GetString("database.prefix") + v.TableName - userTable := viper.GetString("database.prefix") + "accounts" - if err := v.Tx. - Table(table). - Select("*, ? as model_type", v.ColumnName). - Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)). +func GetPostWithAlias(alias string, ignoreLimitation ...bool) (models.Post, error) { + tx := database.C + if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { + tx = FilterPostWithPublishedAt(tx, time.Now()) + } + + var item models.Post + if err := tx. Where("alias = ?", alias). + Preload("Author"). + Preload("Attachments"). First(&item).Error; err != nil { return item, err } - var attachments []models.Attachment - if err := database.C. - Model(&models.Attachment{}). - Where(v.ColumnName+"_id = ?", item.ID). - Scan(&attachments).Error; err != nil { + return item, nil +} + +func GetPost(id uint, ignoreLimitation ...bool) (models.Post, error) { + tx := database.C + if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { + tx = FilterPostWithPublishedAt(tx, time.Now()) + } + + var item models.Post + if err := tx. + Where("id = ?", id). + Preload("Author"). + Preload("Attachments"). + First(&item).Error; err != nil { return item, err - } else { - item.Attachments = attachments } return item, nil } -func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { - var item models.Feed - table := viper.GetString("database.prefix") + v.TableName - userTable := viper.GetString("database.prefix") + "accounts" - if err := v.Tx. - Table(table). - Select("*, ? as model_type", v.ColumnName). - Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)). - Where("id = ?", id).First(&item).Error; err != nil { - return item, err - } - - var attachments []models.Attachment - if err := database.C. - Model(&models.Attachment{}). - Where(v.ColumnName+"_id = ?", id). - Scan(&attachments).Error; err != nil { - return item, err - } else { - item.Attachments = attachments - } - - return item, nil -} - -func (v *PostTypeContext) Count() (int64, error) { +func CountPost(tx *gorm.DB) (int64, error) { var count int64 - table := viper.GetString("database.prefix") + v.TableName - if err := v.Tx.Table(table).Count(&count).Error; err != nil { + if err := tx.Model(&models.Post{}).Count(&count).Error; err != nil { return count, err } return count, nil } -func (v *PostTypeContext) CountComments(id uint) int64 { +func CountPostReply(id uint) int64 { var count int64 - if err := database.C.Model(&models.Comment{}). - Where(v.ColumnName+"_id = ?", id). + if err := database.C.Model(&models.Post{}). + Where("reply_id = ?", id). Count(&count).Error; err != nil { return 0 } @@ -144,10 +101,10 @@ func (v *PostTypeContext) CountComments(id uint) int64 { return count } -func (v *PostTypeContext) CountReactions(id uint) int64 { +func CountPostReactions(id uint) int64 { var count int64 if err := database.C.Model(&models.Reaction{}). - Where(v.ColumnName+"_id = ?", id). + Where("post_id = ?", id). Count(&count).Error; err != nil { return 0 } @@ -155,7 +112,7 @@ func (v *PostTypeContext) CountReactions(id uint) int64 { return count } -func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) { +func ListPostReactions(id uint) (map[string]int64, error) { var reactions []struct { Symbol string Count int64 @@ -163,7 +120,7 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) { if err := database.C.Model(&models.Reaction{}). Select("symbol, COUNT(id) as count"). - Where(v.ColumnName+"_id = ?", id). + Where("post_id = ?", id). Group("symbol"). Scan(&reactions).Error; err != nil { return map[string]int64{}, err @@ -178,21 +135,22 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) { }), nil } -func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models.Feed, error) { +func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]models.Post, error) { if take > 20 { take = 20 } - var items []*models.Feed - table := viper.GetString("database.prefix") + v.TableName - if err := v.Tx. - Table(table). - Select("*, ? as model_type", v.ColumnName). - Limit(take).Offset(offset).Find(&items).Error; err != nil { + var items []models.Post + if err := tx. + Limit(take).Offset(offset). + Order("created_at DESC"). + Preload("Author"). + Preload("Attachments"). + Find(&items).Error; err != nil { return items, err } - idx := lo.Map(items, func(item *models.Feed, index int) uint { + idx := lo.Map(items, func(item models.Post, index int) uint { return item.ID }) @@ -204,14 +162,14 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models } if err := database.C.Model(&models.Reaction{}). - Select(v.ColumnName+"_id as post_id, symbol, COUNT(id) as count"). - Where(v.ColumnName+"_id IN (?)", idx). + Select("post_id as post_id, symbol, COUNT(id) as count"). + Where("post_id IN (?)", idx). Group("post_id, symbol"). Scan(&reactions).Error; err != nil { return items, err } - itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { + itemMap := lo.SliceToMap(items, func(item models.Post) (uint, models.Post) { return item.ID, item }) @@ -230,73 +188,34 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models } } - { - var attachments []struct { - models.Attachment - - PostID uint `json:"post_id"` - } - - itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { - return item.ID, item - }) - - idx := lo.Map(items, func(item *models.Feed, index int) uint { - return item.ID - }) - - if err := database.C. - Model(&models.Attachment{}). - Select(v.ColumnName+"_id as post_id, *"). - Where(v.ColumnName+"_id IN (?)", idx). - Scan(&attachments).Error; err != nil { - return items, err - } - - list := map[uint][]models.Attachment{} - for _, info := range attachments { - list[info.PostID] = append(list[info.PostID], info.Attachment) - } - - for k, v := range list { - if post, ok := itemMap[k]; ok { - post.Attachments = v - } - } - } - return items, nil } -func MapCategoriesAndTags[T models.PostInterface](item T) (T, error) { +func InitPostCategoriesAndTags(item models.Post) (models.Post, error) { var err error - categories := item.GetCategories() - for idx, category := range categories { - categories[idx], err = GetCategory(category.Alias) + for idx, category := range item.Categories { + item.Categories[idx], err = GetCategory(category.Alias) if err != nil { return item, err } } - item.SetCategories(categories) - tags := item.GetHashtags() - for idx, tag := range tags { - tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) + for idx, tag := range item.Tags { + item.Tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) if err != nil { return item, err } } - item.SetHashtags(tags) return item, nil } -func NewPost[T models.PostInterface](item T) (T, error) { - item, err := MapCategoriesAndTags(item) +func NewPost(user models.Account, item models.Post) (models.Post, error) { + item, err := InitPostCategoriesAndTags(item) if err != nil { return item, err } - if item.GetRealm() != nil { - _, err := GetRealmMember(item.GetRealm().ID, item.GetAuthor().ExternalID) + if item.RealmID != nil { + _, err := GetRealmMember(*item.RealmID, user.ExternalID) if err != nil { return item, fmt.Errorf("you aren't a part of that realm: %v", err) } @@ -306,19 +225,20 @@ func NewPost[T models.PostInterface](item T) (T, error) { return item, err } - if item.GetReplyTo() != nil { + // Notify the original poster its post has been replied + if item.ReplyID != nil { go func() { - var op models.Moment + var op models.Post if err := database.C. - Where("id = ?", item.GetReplyTo()). + Where("id = ?", item.ReplyID). 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()) + if op.Author.ID != user.ID { + postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), item.Alias) err := NotifyAccount( op.Author, - fmt.Sprintf("%s replied you", item.GetAuthor().Name), - fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name), + fmt.Sprintf("%s replied you", user.Nick), + fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), false, &proto.NotifyLink{Label: "Related post", Url: postUrl}, ) @@ -330,36 +250,11 @@ func NewPost[T models.PostInterface](item T) (T, error) { }() } - 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 - }) - - for _, account := range accounts { - postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID()) - err := NotifyAccount( - account, - fmt.Sprintf("%s just posted a post", item.GetAuthor().Name), - "Someone you followed post a brand new post. Check it out!", - false, - &proto.NotifyLink{Label: "Related post", Url: postUrl}, - ) - if err != nil { - log.Error().Err(err).Msg("An error occurred when notifying user...") - } - } - }() - } - return item, nil } -func EditPost[T models.PostInterface](item T) (T, error) { - item, err := MapCategoriesAndTags(item) +func EditPost(item models.Post) (models.Post, error) { + item, err := InitPostCategoriesAndTags(item) if err != nil { return item, err } @@ -369,11 +264,11 @@ func EditPost[T models.PostInterface](item T) (T, error) { return item, err } -func DeletePost[T models.PostInterface](item T) error { +func DeletePost(item models.Post) error { return database.C.Delete(&item).Error } -func (v *PostTypeContext) React(reaction models.Reaction) (bool, models.Reaction, error) { +func ReactPost(reaction models.Reaction) (bool, models.Reaction, error) { if err := database.C.Where(reaction).First(&reaction).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return true, reaction, database.C.Save(&reaction).Error