♻️ 一切尽在帖子表 #4
| @@ -11,7 +11,6 @@ var AutoMaintainRange = []any{ | ||||
| 	&models.Category{}, | ||||
| 	&models.Tag{}, | ||||
| 	&models.Post{}, | ||||
| 	&models.Article{}, | ||||
| 	&models.Reaction{}, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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:"-"` | ||||
| } | ||||
| @@ -7,7 +7,6 @@ type Tag struct { | ||||
| 	Name        string `json:"name"` | ||||
| 	Description string `json:"description"` | ||||
| 	Posts       []Post `json:"posts" gorm:"many2many:post_tags"` | ||||
| 	Articles    []Article `json:"articles" gorm:"many2many:article_tags"` | ||||
| } | ||||
|  | ||||
| type Category struct { | ||||
| @@ -17,5 +16,4 @@ type Category struct { | ||||
| 	Name        string `json:"name"` | ||||
| 	Description string `json:"description"` | ||||
| 	Posts       []Post `json:"posts" gorm:"many2many:post_categories"` | ||||
| 	Articles    []Article `json:"articles" gorm:"many2many:article_categories"` | ||||
| } | ||||
|   | ||||
| @@ -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"` | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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, | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user