🗑️ Remove the old article
This commit is contained in:
		@@ -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:"-"`
 | 
			
		||||
}
 | 
			
		||||
@@ -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"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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