From 18dedfc493d0635c93f06335960b3852a30a6863 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 21 Jul 2024 14:47:51 +0800 Subject: [PATCH 1/7] :wastebasket: Remove the old article --- pkg/internal/database/migrator.go | 1 - pkg/internal/models/articles.go | 31 --- pkg/internal/models/categories.go | 18 +- pkg/internal/models/reactions.go | 1 - pkg/internal/server/api/articles_api.go | 281 ------------------------ pkg/internal/server/api/feed_api.go | 50 +---- pkg/internal/server/api/index.go | 11 - pkg/internal/services/articles.go | 234 -------------------- 8 files changed, 12 insertions(+), 615 deletions(-) delete mode 100644 pkg/internal/models/articles.go delete mode 100644 pkg/internal/server/api/articles_api.go delete mode 100644 pkg/internal/services/articles.go diff --git a/pkg/internal/database/migrator.go b/pkg/internal/database/migrator.go index 2ba081e..0b45570 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -11,7 +11,6 @@ var AutoMaintainRange = []any{ &models.Category{}, &models.Tag{}, &models.Post{}, - &models.Article{}, &models.Reaction{}, } diff --git a/pkg/internal/models/articles.go b/pkg/internal/models/articles.go deleted file mode 100644 index 779b4ab..0000000 --- a/pkg/internal/models/articles.go +++ /dev/null @@ -1,31 +0,0 @@ -package models - -import ( - "time" - - "gorm.io/datatypes" -) - -type Article struct { - BaseModel - - Alias string `json:"alias" gorm:"uniqueIndex"` - Title string `json:"title"` - Description string `json:"description"` - Content string `json:"content"` - Language string `json:"language"` - Tags []Tag `json:"tags" gorm:"many2many:article_tags"` - Categories []Category `json:"categories" gorm:"many2many:article_categories"` - Reactions []Reaction `json:"reactions"` - Attachments datatypes.JSONSlice[uint] `json:"attachments"` - RealmID *uint `json:"realm_id"` - Realm *Realm `json:"realm"` - - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - - AuthorID uint `json:"author_id"` - Author Account `json:"author"` - - Metric PostMetric `json:"metric" gorm:"-"` -} diff --git a/pkg/internal/models/categories.go b/pkg/internal/models/categories.go index 30605f7..4cc0d66 100644 --- a/pkg/internal/models/categories.go +++ b/pkg/internal/models/categories.go @@ -3,19 +3,17 @@ package models type Tag struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase"` - Name string `json:"name"` - Description string `json:"description"` - Posts []Post `json:"posts" gorm:"many2many:post_tags"` - Articles []Article `json:"articles" gorm:"many2many:article_tags"` + Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase"` + 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"` - Name string `json:"name"` - Description string `json:"description"` - Posts []Post `json:"posts" gorm:"many2many:post_categories"` - Articles []Article `json:"articles" gorm:"many2many:article_categories"` + Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum"` + Name string `json:"name"` + Description string `json:"description"` + Posts []Post `json:"posts" gorm:"many2many:post_categories"` } diff --git a/pkg/internal/models/reactions.go b/pkg/internal/models/reactions.go index 98231c5..da1762a 100644 --- a/pkg/internal/models/reactions.go +++ b/pkg/internal/models/reactions.go @@ -21,6 +21,5 @@ type Reaction struct { Attitude ReactionAttitude `json:"attitude"` PostID *uint `json:"post_id"` - ArticleID *uint `json:"article_id"` AccountID uint `json:"account_id"` } diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go deleted file mode 100644 index 8b5ef1f..0000000 --- a/pkg/internal/server/api/articles_api.go +++ /dev/null @@ -1,281 +0,0 @@ -package api - -import ( - "fmt" - "strings" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" - "github.com/samber/lo" -) - -func getArticle(c *fiber.Ctx) error { - alias := c.Params("article") - - item, err := services.GetArticleWithAlias(services.FilterPostDraft(database.C), alias) - if err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - item.Metric.ReactionCount = services.CountArticleReactions(item.ID) - item.Metric.ReactionList, err = services.ListResourceReactions(database.C.Where("article_id = ?", item.ID)) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - return c.JSON(item) -} - -func listArticle(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - realmId := c.QueryInt("realmId", 0) - - tx := services.FilterPostDraft(database.C) - if realmId > 0 { - if realm, err := services.GetRealmWithExtID(uint(realmId)); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("realm was not found: %v", err)) - } else { - tx = services.FilterArticleWithRealm(tx, realm.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.FilterArticleWithCategory(tx, c.Query("category")) - } - if len(c.Query("tag")) > 0 { - tx = services.FilterArticleWithTag(tx, c.Query("tag")) - } - - counTx := tx - count, err := services.CountArticle(counTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - items, err := services.ListArticle(tx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(fiber.Map{ - "count": count, - "data": items, - }) -} - -func listDraftArticle(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - tx := services.FilterArticleWithAuthorDraft(database.C, user.ID) - - count, err := services.CountArticle(tx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - items, err := services.ListArticle(tx, take, offset, true) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(fiber.Map{ - "count": count, - "data": items, - }) -} - -func createArticle(c *fiber.Ctx) error { - if err := gap.H.EnsureGrantedPerm(c, "CreateArticles", true); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Title string `json:"title" validate:"required"` - Description string `json:"description"` - Content string `json:"content"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - RealmAlias string `json:"realm"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Alias) == 0 { - data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item := models.Article{ - Alias: data.Alias, - Title: data.Title, - Description: data.Description, - Content: data.Content, - IsDraft: data.IsDraft, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - Tags: data.Tags, - Categories: data.Categories, - Attachments: data.Attachments, - } - - 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.NewArticle(user, item) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(item) -} - -func editArticle(c *fiber.Ctx) error { - id, _ := c.ParamsInt("articleId", 0) - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Title string `json:"title"` - Description string `json:"description"` - Content string `json:"content"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } - - var item models.Article - if err := database.C.Where(models.Article{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item.Alias = data.Alias - item.Title = data.Title - item.Description = data.Description - item.Content = data.Content - item.IsDraft = data.IsDraft - item.PublishedAt = data.PublishedAt - item.Tags = data.Tags - item.Categories = data.Categories - item.Attachments = data.Attachments - - if item, err := services.EditArticle(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - -func deleteArticle(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - id, _ := c.ParamsInt("articleId", 0) - - var item models.Article - if err := database.C.Where(models.Article{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - if err := services.DeleteArticle(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.SendStatus(fiber.StatusOK) -} - -func reactArticle(c *fiber.Ctx) error { - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Symbol string `json:"symbol"` - Attitude models.ReactionAttitude `json:"attitude"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } - - reaction := models.Reaction{ - Symbol: data.Symbol, - Attitude: data.Attitude, - AccountID: user.ID, - } - - alias := c.Params("article") - - var res models.Article - if err := database.C.Where("alias = ?", alias).Select("id").First(&res).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find article to react: %v", err)) - } else { - reaction.ArticleID = &res.ID - } - - if positive, reaction, err := services.ReactArticle(user, 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/internal/server/api/feed_api.go b/pkg/internal/server/api/feed_api.go index b6ab05e..2264a9e 100644 --- a/pkg/internal/server/api/feed_api.go +++ b/pkg/internal/server/api/feed_api.go @@ -2,7 +2,6 @@ package api import ( "fmt" - "sort" "time" "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" @@ -25,14 +24,12 @@ func listFeed(c *fiber.Ctx) error { realmId := c.QueryInt("realmId", 0) postTx := services.FilterPostDraft(database.C) - articleTx := services.FilterArticleDraft(database.C) if realmId > 0 { if realm, err := services.GetRealmWithExtID(uint(realmId)); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("realm was not found: %v", err)) } else { postTx = services.FilterPostWithRealm(postTx, realm.ID) - articleTx = services.FilterArticleWithRealm(articleTx, realm.ID) } } @@ -42,45 +39,33 @@ func listFeed(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, err.Error()) } postTx = postTx.Where("author_id = ?", author.ID) - articleTx = articleTx.Where("author_id = ?", author.ID) } if len(c.Query("category")) > 0 { postTx = services.FilterPostWithCategory(postTx, c.Query("category")) - articleTx = services.FilterArticleWithCategory(articleTx, c.Query("category")) } if len(c.Query("tag")) > 0 { postTx = services.FilterPostWithTag(postTx, c.Query("tag")) - articleTx = services.FilterArticleWithTag(articleTx, c.Query("tag")) } postCountTx := postTx - articleCountTx := articleTx postCount, err := services.CountPost(postCountTx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - articleCount, err := services.CountArticle(articleCountTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } postItems, err := services.ListPost(postTx, take, offset) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - articleItems, err := services.ListArticle(articleTx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } var feed []FeedRecord encodeToFeed := func(t string, in any, createdAt time.Time) FeedRecord { var result map[string]any raw, _ := jsoniter.Marshal(in) - jsoniter.Unmarshal(raw, &result) + _ = jsoniter.Unmarshal(raw, &result) return FeedRecord{ Type: t, @@ -93,16 +78,8 @@ func listFeed(c *fiber.Ctx) error { feed = append(feed, encodeToFeed("post", post, post.CreatedAt)) } - for _, article := range articleItems { - feed = append(feed, encodeToFeed("article", article, article.CreatedAt)) - } - - sort.Slice(feed, func(i, j int) bool { - return feed[i].CreatedAt.After(feed[j].CreatedAt) - }) - return c.JSON(fiber.Map{ - "count": postCount + articleCount, + "count": postCount, "data": feed, }) } @@ -117,35 +94,24 @@ func listDraftMixed(c *fiber.Ctx) error { user := c.Locals("user").(models.Account) postTx := services.FilterPostWithAuthorDraft(database.C, user.ID) - articleTx := services.FilterArticleWithAuthorDraft(database.C, user.ID) - postCountTx := postTx - articleCountTx := articleTx postCount, err := services.CountPost(postCountTx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - articleCount, err := services.CountArticle(articleCountTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } postItems, err := services.ListPost(postTx, take, offset) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - articleItems, err := services.ListArticle(articleTx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } var feed []FeedRecord encodeToFeed := func(t string, in any, createdAt time.Time) FeedRecord { var result map[string]any raw, _ := jsoniter.Marshal(in) - jsoniter.Unmarshal(raw, &result) + _ = jsoniter.Unmarshal(raw, &result) return FeedRecord{ Type: t, @@ -158,16 +124,8 @@ func listDraftMixed(c *fiber.Ctx) error { feed = append(feed, encodeToFeed("post", post, post.CreatedAt)) } - for _, article := range articleItems { - feed = append(feed, encodeToFeed("article", article, article.CreatedAt)) - } - - sort.Slice(feed, func(i, j int) bool { - return feed[i].CreatedAt.After(feed[j].CreatedAt) - }) - return c.JSON(fiber.Map{ - "count": postCount + articleCount, + "count": postCount, "data": feed, }) } diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 6f483de..21db98c 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -16,7 +16,6 @@ func MapAPIs(app *fiber.App, baseURL string) { { drafts.Get("/", listDraftMixed) drafts.Get("/posts", listDraftPost) - drafts.Get("/articles", listDraftArticle) } posts := api.Group("/posts").Name("Posts API") @@ -31,16 +30,6 @@ func MapAPIs(app *fiber.App, baseURL string) { posts.Get("/:post/replies", listPostReplies) } - articles := api.Group("/articles").Name("Articles API") - { - articles.Get("/", listArticle) - articles.Get("/:article", getArticle) - articles.Post("/", createArticle) - articles.Post("/:article/react", reactArticle) - articles.Put("/:articleId", editArticle) - articles.Delete("/:articleId", deleteArticle) - } - api.Get("/categories", listCategories) api.Post("/categories", newCategory) api.Put("/categories/:categoryId", editCategory) diff --git a/pkg/internal/services/articles.go b/pkg/internal/services/articles.go deleted file mode 100644 index 84835d5..0000000 --- a/pkg/internal/services/articles.go +++ /dev/null @@ -1,234 +0,0 @@ -package services - -import ( - "errors" - "fmt" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" - "github.com/rs/zerolog/log" - "github.com/samber/lo" - "github.com/spf13/viper" - "gorm.io/gorm" -) - -func FilterArticleWithCategory(tx *gorm.DB, alias string) *gorm.DB { - prefix := viper.GetString("database.prefix") - return tx.Joins(fmt.Sprintf("JOIN %sarticle_categories ON %sarticles.id = %sarticle_categories.article_id", prefix, prefix, prefix)). - Joins(fmt.Sprintf("JOIN %scategories ON %scategories.id = %sarticle_categories.category_id", prefix, prefix, prefix)). - Where(fmt.Sprintf("%scategories.alias = ?", prefix), alias) -} - -func FilterArticleWithTag(tx *gorm.DB, alias string) *gorm.DB { - prefix := viper.GetString("database.prefix") - return tx.Joins(fmt.Sprintf("JOIN %sarticle_tags ON %sarticles.id = %sarticle_tags.article_id", prefix, prefix, prefix)). - Joins(fmt.Sprintf("JOIN %stags ON %stags.id = %sarticle_tags.tag_id", prefix, prefix, prefix)). - Where(fmt.Sprintf("%stags.alias = ?", prefix), alias) -} - -func FilterArticleWithRealm(tx *gorm.DB, id uint) *gorm.DB { - if id > 0 { - return tx.Where("realm_id = ?", id) - } else { - return tx.Where("realm_id IS NULL") - } -} - -func FilterArticleWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB { - return tx.Where("published_at <= ? OR published_at IS NULL", date) -} - -func FilterArticleWithAuthorDraft(tx *gorm.DB, uid uint) *gorm.DB { - return tx.Where("author_id = ? AND is_draft = ?", uid, true) -} - -func FilterArticleDraft(tx *gorm.DB) *gorm.DB { - return tx.Where("is_draft = ? OR is_draft IS NULL", false) -} - -func GetArticleWithAlias(tx *gorm.DB, alias string, ignoreLimitation ...bool) (models.Article, error) { - if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { - tx = FilterArticleWithPublishedAt(tx, time.Now()) - } - - var item models.Article - if err := tx. - Where("alias = ?", alias). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - First(&item).Error; err != nil { - return item, err - } - - return item, nil -} - -func GetArticle(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Article, error) { - if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { - tx = FilterArticleWithPublishedAt(tx, time.Now()) - } - - var item models.Article - if err := tx. - Where("id = ?", id). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - First(&item).Error; err != nil { - return item, err - } - - return item, nil -} - -func CountArticle(tx *gorm.DB) (int64, error) { - var count int64 - if err := tx.Model(&models.Article{}).Count(&count).Error; err != nil { - return count, err - } - - return count, nil -} - -func CountArticleReactions(id uint) int64 { - var count int64 - if err := database.C.Model(&models.Reaction{}). - Where("article_id = ?", id). - Count(&count).Error; err != nil { - return 0 - } - - return count -} - -func ListArticle(tx *gorm.DB, take int, offset int, noReact ...bool) ([]*models.Article, error) { - if take > 100 { - take = 100 - } - - var items []*models.Article - if err := tx. - Limit(take).Offset(offset). - Order("created_at DESC"). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - Find(&items).Error; err != nil { - return items, err - } - - idx := lo.Map(items, func(item *models.Article, index int) uint { - return item.ID - }) - - // Load reactions - if len(noReact) <= 0 || !noReact[0] { - if mapping, err := BatchListResourceReactions(database.C.Where("article_id IN ?", idx), "article_id"); err != nil { - return items, err - } else { - itemMap := lo.SliceToMap(items, func(item *models.Article) (uint, *models.Article) { - return item.ID, item - }) - - for k, v := range mapping { - if post, ok := itemMap[k]; ok { - post.Metric = models.PostMetric{ - ReactionList: v, - } - } - } - } - } - - return items, nil -} - -func EnsureArticleCategoriesAndTags(item models.Article) (models.Article, error) { - var err error - for idx, category := range item.Categories { - item.Categories[idx], err = GetCategory(category.Alias) - if err != nil { - return item, err - } - } - for idx, tag := range item.Tags { - item.Tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) - if err != nil { - return item, err - } - } - return item, nil -} - -func NewArticle(user models.Account, item models.Article) (models.Article, error) { - item.Language = DetectLanguage(&item.Content) - - item, err := EnsureArticleCategoriesAndTags(item) - if err != nil { - return item, err - } - - 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) - } - } - - if err := database.C.Save(&item).Error; err != nil { - return item, err - } - - return item, nil -} - -func EditArticle(item models.Article) (models.Article, error) { - item.Language = DetectLanguage(&item.Content) - item, err := EnsureArticleCategoriesAndTags(item) - if err != nil { - return item, err - } - - err = database.C.Save(&item).Error - - return item, err -} - -func DeleteArticle(item models.Article) error { - return database.C.Delete(&item).Error -} - -func ReactArticle(user models.Account, reaction models.Reaction) (bool, models.Reaction, error) { - if err := database.C.Where(reaction).First(&reaction).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - var op models.Article - if err := database.C. - Where("id = ?", reaction.ArticleID). - Preload("Author"). - First(&op).Error; err == nil { - if op.Author.ID != user.ID { - err = NotifyPosterAccount( - op.Author, - "Article got reacted", - fmt.Sprintf("%s (%s) reacted your article a %s", user.Nick, user.Name, reaction.Symbol), - lo.ToPtr(fmt.Sprintf("%s reacted your article", user.Nick)), - ) - if err != nil { - log.Error().Err(err).Msg("An error occurred when notifying user...") - } - } - } - - return true, reaction, database.C.Save(&reaction).Error - } else { - return true, reaction, err - } - } else { - return false, reaction, database.C.Delete(&reaction).Error - } -} -- 2.45.2 From 3a5a84ae56219632973bad5672aec6ec96bfdcdf Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 00:49:36 +0800 Subject: [PATCH 2/7] :sparkles: New story & article create & edit api --- pkg/internal/models/posts.go | 40 ++++--- pkg/internal/server/api/articles_api.go | 126 ++++++++++++++++++++ pkg/internal/server/api/index.go | 17 ++- pkg/internal/server/api/posts_api.go | 135 +--------------------- pkg/internal/server/api/stories_api.go | 146 ++++++++++++++++++++++++ pkg/internal/services/attachments.go | 21 ---- pkg/internal/services/posts.go | 38 +----- 7 files changed, 317 insertions(+), 206 deletions(-) create mode 100644 pkg/internal/server/api/articles_api.go create mode 100644 pkg/internal/server/api/stories_api.go delete mode 100644 pkg/internal/services/attachments.go diff --git a/pkg/internal/models/posts.go b/pkg/internal/models/posts.go index 191f5a5..f15eb66 100644 --- a/pkg/internal/models/posts.go +++ b/pkg/internal/models/posts.go @@ -9,20 +9,18 @@ import ( type Post struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` - Content *string `json:"content"` - Language string `json:"language"` - Tags []Tag `json:"tags" gorm:"many2many:post_tags"` - Categories []Category `json:"categories" gorm:"many2many:post_categories"` - Reactions []Reaction `json:"reactions"` - Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"` - Attachments datatypes.JSONSlice[uint] `json:"attachments"` - 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"` + Body datatypes.JSONMap `json:"body"` + Language string `json:"language"` + Tags []Tag `json:"tags" gorm:"many2many:post_tags"` + Categories []Category `json:"categories" gorm:"many2many:post_categories"` + Reactions []Reaction `json:"reactions"` + 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"` IsDraft bool `json:"is_draft"` PublishedAt *time.Time `json:"published_at"` @@ -32,3 +30,17 @@ type Post struct { Metric PostMetric `json:"metric" gorm:"-"` } + +type PostStoryBody struct { + Title *string `json:"title"` + Content string `json:"content"` + Location *string `json:"location"` + Attachments []uint `json:"attachments"` +} + +type PostArticleBody struct { + Title string `json:"title"` + Description *string `json:"description"` + Content string `json:"content"` + Attachments []uint `json:"attachments"` +} diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go new file mode 100644 index 0000000..5d87551 --- /dev/null +++ b/pkg/internal/server/api/articles_api.go @@ -0,0 +1,126 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" + "time" +) + +func createArticle(c *fiber.Ctx) error { + if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + PublishedAt *time.Time `json:"published_at"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + body := models.PostArticleBody{ + Title: data.Title, + Description: data.Description, + Content: data.Content, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item := models.Post{ + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + IsDraft: data.IsDraft, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + } + + if data.RealmAlias != nil { + 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("unable to post in the realm, access denied: %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 editArticle(c *fiber.Ctx) error { + id, _ := c.ParamsInt("postId", 0) + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.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()) + } + + body := models.PostArticleBody{ + Title: data.Title, + Content: data.Content, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item.Body = bodyMapping + item.IsDraft = data.IsDraft + item.PublishedAt = data.PublishedAt + item.Tags = data.Tags + item.Categories = data.Categories + + if item, err := services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) + } +} diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 21db98c..5760e24 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -18,13 +18,22 @@ func MapAPIs(app *fiber.App, baseURL string) { drafts.Get("/posts", listDraftPost) } + stories := api.Group("/stories").Name("Story API") + { + stories.Post("/", createStory) + stories.Put("/:postId", editStory) + } + articles := api.Group("/articles").Name("Article API") + { + articles.Post("/", createArticle) + articles.Put("/:articleId", editArticle) + } + posts := api.Group("/posts").Name("Posts API") { posts.Get("/", listPost) - posts.Get("/:post", getPost) - posts.Post("/", createPost) - posts.Post("/:post/react", reactPost) - posts.Put("/:postId", editPost) + posts.Get("/:postId", getPost) + posts.Post("/:postId/react", reactPost) posts.Delete("/:postId", deletePost) posts.Get("/:post/replies", listPostReplies) diff --git a/pkg/internal/server/api/posts_api.go b/pkg/internal/server/api/posts_api.go index 8662535..6ceaac6 100644 --- a/pkg/internal/server/api/posts_api.go +++ b/pkg/internal/server/api/posts_api.go @@ -2,23 +2,19 @@ package api import ( "fmt" - "strings" - "time" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" "github.com/samber/lo" ) func getPost(c *fiber.Ctx) error { - alias := c.Params("post") + id, _ := c.ParamsInt("postId") - item, err := services.GetPostWithAlias(services.FilterPostDraft(database.C), alias) + item, err := services.GetPost(services.FilterPostDraft(database.C), uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -108,133 +104,6 @@ func listDraftPost(c *fiber.Ctx) error { }) } -func createPost(c *fiber.Ctx) error { - if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Content string `json:"content" validate:"required,max=4096"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - RealmAlias string `json:"realm"` - ReplyTo *uint `json:"reply_to"` - RepostTo *uint `json:"repost_to"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Alias) == 0 { - data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item := models.Post{ - Alias: data.Alias, - Content: &data.Content, - Tags: data.Tags, - Categories: data.Categories, - Attachments: data.Attachments, - IsDraft: data.IsDraft, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - } - - if data.ReplyTo != nil { - var replyTo models.Post - if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) - } else { - item.ReplyID = &replyTo.ID - } - } - if data.RepostTo != nil { - var repostTo models.Post - if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) - } else { - item.RepostID = &repostTo.ID - } - } - - 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 { - id, _ := c.ParamsInt("postId", 0) - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Content string `json:"content" validate:"required,max=4096"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - } - - if err := exts.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()) - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item.Content = &data.Content - item.Alias = data.Alias - item.IsDraft = data.IsDraft - 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 { if err := gap.H.EnsureAuthenticated(c); err != nil { return err diff --git a/pkg/internal/server/api/stories_api.go b/pkg/internal/server/api/stories_api.go new file mode 100644 index 0000000..df35f48 --- /dev/null +++ b/pkg/internal/server/api/stories_api.go @@ -0,0 +1,146 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" + "time" +) + +func createStory(c *fiber.Ctx) error { + if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` + ReplyTo *uint `json:"reply_to"` + RepostTo *uint `json:"repost_to"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + body := models.PostStoryBody{ + Title: data.Title, + Content: data.Content, + Location: data.Location, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item := models.Post{ + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + IsDraft: data.IsDraft, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + } + + if data.ReplyTo != nil { + var replyTo models.Post + if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) + } else { + item.ReplyID = &replyTo.ID + } + } + if data.RepostTo != nil { + var repostTo models.Post + if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) + } else { + item.RepostID = &repostTo.ID + } + } + + if data.RealmAlias != nil { + 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("unable to post in the realm, access denied: %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 editStory(c *fiber.Ctx) error { + id, _ := c.ParamsInt("postId", 0) + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.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()) + } + + body := models.PostStoryBody{ + Title: data.Title, + Content: data.Content, + Location: data.Location, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item.Body = bodyMapping + item.IsDraft = data.IsDraft + item.PublishedAt = data.PublishedAt + item.Tags = data.Tags + item.Categories = data.Categories + + if item, err := services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) + } +} diff --git a/pkg/internal/services/attachments.go b/pkg/internal/services/attachments.go deleted file mode 100644 index 6ab544a..0000000 --- a/pkg/internal/services/attachments.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "context" - "git.solsynth.dev/hydrogen/dealer/pkg/hyper" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" - "git.solsynth.dev/hydrogen/paperclip/pkg/proto" - "github.com/samber/lo" -) - -func CheckAttachmentByIDExists(id uint, usage string) bool { - pc, err := gap.H.GetServiceGrpcConn(hyper.ServiceTypeFileProvider) - if err != nil { - return false - } - _, err = proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{ - Id: lo.ToPtr(uint64(id)), - Usage: &usage, - }) - return err == nil -} diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index f56d0c8..998f3a2 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -55,33 +55,6 @@ func FilterPostDraft(tx *gorm.DB) *gorm.DB { return tx.Where("is_draft = ? OR is_draft IS NULL", false) } -func GetPostWithAlias(tx *gorm.DB, alias string, ignoreLimitation ...bool) (models.Post, error) { - if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { - tx = FilterPostWithPublishedAt(tx, time.Now()) - } - - var item models.Post - if err := tx. - Where("alias = ?", alias). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - Preload("ReplyTo"). - Preload("ReplyTo.Author"). - Preload("ReplyTo.Tags"). - Preload("ReplyTo.Categories"). - Preload("RepostTo"). - Preload("RepostTo.Author"). - Preload("RepostTo.Tags"). - Preload("RepostTo.Categories"). - First(&item).Error; err != nil { - return item, err - } - - return item, nil -} - func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error) { if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { tx = FilterPostWithPublishedAt(tx, time.Now()) @@ -243,8 +216,6 @@ func EnsurePostCategoriesAndTags(item models.Post) (models.Post, error) { } func NewPost(user models.Account, item models.Post) (models.Post, error) { - item.Language = DetectLanguage(item.Content) - item, err := EnsurePostCategoriesAndTags(item) if err != nil { return item, err @@ -272,7 +243,7 @@ func NewPost(user models.Account, item models.Post) (models.Post, error) { err = NotifyPosterAccount( op.Author, "Post got replied", - fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), + fmt.Sprintf("%s (%s) replied your post.", user.Nick, user.Name), lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), ) if err != nil { @@ -286,7 +257,6 @@ func NewPost(user models.Account, item models.Post) (models.Post, error) { } func EditPost(item models.Post) (models.Post, error) { - item.Language = DetectLanguage(item.Content) item, err := EnsurePostCategoriesAndTags(item) if err != nil { return item, err @@ -312,9 +282,9 @@ func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reac if op.Author.ID != user.ID { err = NotifyPosterAccount( op.Author, - "Post got replied", - fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), - lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), + "Post got reacted", + fmt.Sprintf("%s (%s) reacted your post a %s.", user.Nick, user.Name, reaction.Symbol), + lo.ToPtr(fmt.Sprintf("%s reacted you", user.Nick)), ) if err != nil { log.Error().Err(err).Msg("An error occurred when notifying user...") -- 2.45.2 From 045744aa18aea252dea941c8d1be9e076cfc2b0c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 01:19:23 +0800 Subject: [PATCH 3/7] :sparkles: Post type --- pkg/internal/models/posts.go | 6 ++++++ pkg/internal/server/api/articles_api.go | 1 + pkg/internal/server/api/stories_api.go | 1 + 3 files changed, 8 insertions(+) diff --git a/pkg/internal/models/posts.go b/pkg/internal/models/posts.go index f15eb66..03f90e8 100644 --- a/pkg/internal/models/posts.go +++ b/pkg/internal/models/posts.go @@ -6,9 +6,15 @@ import ( "gorm.io/datatypes" ) +const ( + PostTypeStory = "story" + PostTypeArticle = "article" +) + type Post struct { BaseModel + Type string `json:"type"` Body datatypes.JSONMap `json:"body"` Language string `json:"language"` Tags []Tag `json:"tags" gorm:"many2many:post_tags"` diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go index 5d87551..5f8d070 100644 --- a/pkg/internal/server/api/articles_api.go +++ b/pkg/internal/server/api/articles_api.go @@ -46,6 +46,7 @@ func createArticle(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item := models.Post{ + Type: models.PostTypeArticle, Body: bodyMapping, Tags: data.Tags, Categories: data.Categories, diff --git a/pkg/internal/server/api/stories_api.go b/pkg/internal/server/api/stories_api.go index df35f48..655114c 100644 --- a/pkg/internal/server/api/stories_api.go +++ b/pkg/internal/server/api/stories_api.go @@ -48,6 +48,7 @@ func createStory(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item := models.Post{ + Type: models.PostTypeStory, Body: bodyMapping, Tags: data.Tags, Categories: data.Categories, -- 2.45.2 From f5664715f81a85ce1ce04318e703ed58e933e988 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 01:44:04 +0800 Subject: [PATCH 4/7] :sparkles: Publish until --- pkg/internal/models/posts.go | 5 ++- pkg/internal/server/api/articles_api.go | 56 ++++++++++++----------- pkg/internal/server/api/stories_api.go | 60 +++++++++++++------------ pkg/internal/services/posts.go | 4 +- 4 files changed, 68 insertions(+), 57 deletions(-) diff --git a/pkg/internal/models/posts.go b/pkg/internal/models/posts.go index 03f90e8..e506429 100644 --- a/pkg/internal/models/posts.go +++ b/pkg/internal/models/posts.go @@ -28,8 +28,9 @@ type Post struct { RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"` Realm *Realm `json:"realm"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` AuthorID uint `json:"author_id"` Author Account `json:"author"` diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go index 5f8d070..abc03f2 100644 --- a/pkg/internal/server/api/articles_api.go +++ b/pkg/internal/server/api/articles_api.go @@ -19,15 +19,16 @@ func createArticle(c *fiber.Ctx) error { user := c.Locals("user").(models.Account) var data struct { - Title string `json:"title" validate:"required,max=1024"` - Description *string `json:"description" validate:"max=2048"` - Content string `json:"content" validate:"required"` - Attachments []uint `json:"attachments"` - PublishedAt *time.Time `json:"published_at"` - IsDraft bool `json:"is_draft"` - RealmAlias *string `json:"realm"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` } if err := exts.BindAndValidate(c, &data); err != nil { @@ -46,13 +47,14 @@ func createArticle(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item := models.Post{ - Type: models.PostTypeArticle, - Body: bodyMapping, - Tags: data.Tags, - Categories: data.Categories, - IsDraft: data.IsDraft, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, + Type: models.PostTypeArticle, + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + IsDraft: data.IsDraft, + PublishedAt: data.PublishedAt, + PublishedUntil: data.PublishedUntil, + AuthorID: user.ID, } if data.RealmAlias != nil { @@ -81,14 +83,15 @@ func editArticle(c *fiber.Ctx) error { user := c.Locals("user").(models.Account) var data struct { - Title string `json:"title" validate:"required,max=1024"` - Description *string `json:"description" validate:"max=2048"` - Content string `json:"content" validate:"required"` - Attachments []uint `json:"attachments"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` } if err := exts.BindAndValidate(c, &data); err != nil { @@ -114,10 +117,11 @@ func editArticle(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item.Body = bodyMapping - item.IsDraft = data.IsDraft - item.PublishedAt = data.PublishedAt item.Tags = data.Tags item.Categories = data.Categories + item.IsDraft = data.IsDraft + item.PublishedAt = data.PublishedAt + item.PublishedUntil = data.PublishedUntil if item, err := services.EditPost(item); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) diff --git a/pkg/internal/server/api/stories_api.go b/pkg/internal/server/api/stories_api.go index 655114c..e6ae11b 100644 --- a/pkg/internal/server/api/stories_api.go +++ b/pkg/internal/server/api/stories_api.go @@ -19,17 +19,18 @@ func createStory(c *fiber.Ctx) error { user := c.Locals("user").(models.Account) var data struct { - Title *string `json:"title" validate:"max=1024"` - Content string `json:"content" validate:"required,max=4096"` - Location *string `json:"location" validate:"max=2048"` - Attachments []uint `json:"attachments"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - PublishedAt *time.Time `json:"published_at"` - IsDraft bool `json:"is_draft"` - RealmAlias *string `json:"realm"` - ReplyTo *uint `json:"reply_to"` - RepostTo *uint `json:"repost_to"` + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` + ReplyTo *uint `json:"reply_to"` + RepostTo *uint `json:"repost_to"` } if err := exts.BindAndValidate(c, &data); err != nil { @@ -48,13 +49,14 @@ func createStory(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item := models.Post{ - Type: models.PostTypeStory, - Body: bodyMapping, - Tags: data.Tags, - Categories: data.Categories, - IsDraft: data.IsDraft, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, + Type: models.PostTypeStory, + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + PublishedAt: data.PublishedAt, + PublishedUntil: data.PublishedUntil, + IsDraft: data.IsDraft, + AuthorID: user.ID, } if data.ReplyTo != nil { @@ -100,14 +102,15 @@ func editStory(c *fiber.Ctx) error { user := c.Locals("user").(models.Account) var data struct { - Title *string `json:"title" validate:"max=1024"` - Content string `json:"content" validate:"required,max=4096"` - Location *string `json:"location" validate:"max=2048"` - Attachments []uint `json:"attachments"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` + IsDraft bool `json:"is_draft"` } if err := exts.BindAndValidate(c, &data); err != nil { @@ -134,10 +137,11 @@ func editStory(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item.Body = bodyMapping - item.IsDraft = data.IsDraft - item.PublishedAt = data.PublishedAt item.Tags = data.Tags item.Categories = data.Categories + item.PublishedAt = data.PublishedAt + item.PublishedUntil = data.PublishedUntil + item.IsDraft = data.IsDraft if item, err := services.EditPost(item); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index 998f3a2..ec7970a 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -44,7 +44,9 @@ func FilterPostReply(tx *gorm.DB, replyTo ...uint) *gorm.DB { } func FilterPostWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB { - return tx.Where("published_at <= ? OR published_at IS NULL", date) + return tx. + Where("published_at <= ? OR published_at IS NULL", date). + Where("published_until > ? OR published_until IS NULL", date) } func FilterPostWithAuthorDraft(tx *gorm.DB, uid uint) *gorm.DB { -- 2.45.2 From 96b36c1db478fd34e419c955b9c70acd096ec663 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 01:44:40 +0800 Subject: [PATCH 5/7] :sparkles: Sort based on published at --- pkg/internal/services/posts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index ec7970a..52303fb 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -123,7 +123,7 @@ func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]*models.Pos var items []*models.Post if err := tx. Limit(take).Offset(offset). - Order("created_at DESC"). + Order("published_at DESC"). Preload("Tags"). Preload("Categories"). Preload("Realm"). -- 2.45.2 From d4dfa810d1152cc0d67bba21286acaa72247badd Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 01:46:38 +0800 Subject: [PATCH 6/7] :sparkles: Detect language in controller layer --- pkg/internal/server/api/articles_api.go | 2 ++ pkg/internal/server/api/stories_api.go | 2 ++ pkg/internal/services/languages.go | 14 ++++++-------- pkg/internal/services/posts.go | 4 ++++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go index abc03f2..6b9982d 100644 --- a/pkg/internal/server/api/articles_api.go +++ b/pkg/internal/server/api/articles_api.go @@ -49,6 +49,7 @@ func createArticle(c *fiber.Ctx) error { item := models.Post{ Type: models.PostTypeArticle, Body: bodyMapping, + Language: services.DetectLanguage(data.Content), Tags: data.Tags, Categories: data.Categories, IsDraft: data.IsDraft, @@ -117,6 +118,7 @@ func editArticle(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item.Body = bodyMapping + item.Language = services.DetectLanguage(data.Content) item.Tags = data.Tags item.Categories = data.Categories item.IsDraft = data.IsDraft diff --git a/pkg/internal/server/api/stories_api.go b/pkg/internal/server/api/stories_api.go index e6ae11b..b38f2e8 100644 --- a/pkg/internal/server/api/stories_api.go +++ b/pkg/internal/server/api/stories_api.go @@ -51,6 +51,7 @@ func createStory(c *fiber.Ctx) error { item := models.Post{ Type: models.PostTypeStory, Body: bodyMapping, + Language: services.DetectLanguage(data.Content), Tags: data.Tags, Categories: data.Categories, PublishedAt: data.PublishedAt, @@ -137,6 +138,7 @@ func editStory(c *fiber.Ctx) error { _ = jsoniter.Unmarshal(rawBody, &bodyMapping) item.Body = bodyMapping + item.Language = services.DetectLanguage(data.Content) item.Tags = data.Tags item.Categories = data.Categories item.PublishedAt = data.PublishedAt diff --git a/pkg/internal/services/languages.go b/pkg/internal/services/languages.go index f244290..36e9503 100644 --- a/pkg/internal/services/languages.go +++ b/pkg/internal/services/languages.go @@ -5,14 +5,12 @@ import ( "strings" ) -func DetectLanguage(content *string) string { - if content != nil { - detector := lingua.NewLanguageDetectorBuilder(). - FromLanguages(lingua.AllLanguages()...). - Build() - if lang, ok := detector.DetectLanguageOf(*content); ok { - return strings.ToLower(lang.String()) - } +func DetectLanguage(content string) string { + detector := lingua.NewLanguageDetectorBuilder(). + FromLanguages(lingua.AllLanguages()...). + Build() + if lang, ok := detector.DetectLanguageOf(content); ok { + return strings.ToLower(lang.String()) } return "unknown" } diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index 52303fb..ca9a52c 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -218,6 +218,10 @@ func EnsurePostCategoriesAndTags(item models.Post) (models.Post, error) { } func NewPost(user models.Account, item models.Post) (models.Post, error) { + if !item.IsDraft && item.PublishedAt == nil { + item.PublishedAt = lo.ToPtr(time.Now()) + } + item, err := EnsurePostCategoriesAndTags(item) if err != nil { return item, err -- 2.45.2 From 7b8ca225a8ae05523d8e285968d65defdecb0e7a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 22 Jul 2024 13:41:35 +0800 Subject: [PATCH 7/7] :wastebasket: Remove feed api --- pkg/internal/server/api/feed_api.go | 131 ---------------------------- pkg/internal/server/api/index.go | 9 +- 2 files changed, 1 insertion(+), 139 deletions(-) delete mode 100644 pkg/internal/server/api/feed_api.go diff --git a/pkg/internal/server/api/feed_api.go b/pkg/internal/server/api/feed_api.go deleted file mode 100644 index 2264a9e..0000000 --- a/pkg/internal/server/api/feed_api.go +++ /dev/null @@ -1,131 +0,0 @@ -package api - -import ( - "fmt" - "time" - - "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" - "github.com/gofiber/fiber/v2" - jsoniter "github.com/json-iterator/go" -) - -type FeedRecord struct { - Type string `json:"type"` - Data map[string]any `json:"data"` - CreatedAt time.Time `json:"created_at"` -} - -func listFeed(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - realmId := c.QueryInt("realmId", 0) - - postTx := services.FilterPostDraft(database.C) - - if realmId > 0 { - if realm, err := services.GetRealmWithExtID(uint(realmId)); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("realm was not found: %v", err)) - } else { - postTx = services.FilterPostWithRealm(postTx, realm.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()) - } - postTx = postTx.Where("author_id = ?", author.ID) - } - - if len(c.Query("category")) > 0 { - postTx = services.FilterPostWithCategory(postTx, c.Query("category")) - } - if len(c.Query("tag")) > 0 { - postTx = services.FilterPostWithTag(postTx, c.Query("tag")) - } - - postCountTx := postTx - - postCount, err := services.CountPost(postCountTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - postItems, err := services.ListPost(postTx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - var feed []FeedRecord - - encodeToFeed := func(t string, in any, createdAt time.Time) FeedRecord { - var result map[string]any - raw, _ := jsoniter.Marshal(in) - _ = jsoniter.Unmarshal(raw, &result) - - return FeedRecord{ - Type: t, - Data: result, - CreatedAt: createdAt, - } - } - - for _, post := range postItems { - feed = append(feed, encodeToFeed("post", post, post.CreatedAt)) - } - - return c.JSON(fiber.Map{ - "count": postCount, - "data": feed, - }) -} - -func listDraftMixed(c *fiber.Ctx) error { - take := c.QueryInt("take", 0) - offset := c.QueryInt("offset", 0) - - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - postTx := services.FilterPostWithAuthorDraft(database.C, user.ID) - postCountTx := postTx - - postCount, err := services.CountPost(postCountTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - postItems, err := services.ListPost(postTx, take, offset) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - var feed []FeedRecord - - encodeToFeed := func(t string, in any, createdAt time.Time) FeedRecord { - var result map[string]any - raw, _ := jsoniter.Marshal(in) - _ = jsoniter.Unmarshal(raw, &result) - - return FeedRecord{ - Type: t, - Data: result, - CreatedAt: createdAt, - } - } - - for _, post := range postItems { - feed = append(feed, encodeToFeed("post", post, post.CreatedAt)) - } - - return c.JSON(fiber.Map{ - "count": postCount, - "data": feed, - }) -} diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 5760e24..5ff97c7 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -10,14 +10,6 @@ func MapAPIs(app *fiber.App, baseURL string) { api.Get("/users/me", getUserinfo) api.Get("/users/:accountId", getOthersInfo) - api.Get("/feed", listFeed) - - drafts := api.Group("/drafts").Name("Draft box API") - { - drafts.Get("/", listDraftMixed) - drafts.Get("/posts", listDraftPost) - } - stories := api.Group("/stories").Name("Story API") { stories.Post("/", createStory) @@ -32,6 +24,7 @@ func MapAPIs(app *fiber.App, baseURL string) { posts := api.Group("/posts").Name("Posts API") { posts.Get("/", listPost) + posts.Get("/drafts", listDraftPost) posts.Get("/:postId", getPost) posts.Post("/:postId/react", reactPost) posts.Delete("/:postId", deletePost) -- 2.45.2