♻️ 一切尽在帖子表 #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"` | ||||
| } | ||||
|   | ||||
| @@ -6,17 +6,21 @@ import ( | ||||
| 	"gorm.io/datatypes" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	PostTypeStory   = "story" | ||||
| 	PostTypeArticle = "article" | ||||
| ) | ||||
|  | ||||
| type Post struct { | ||||
| 	BaseModel | ||||
|  | ||||
| 	Alias       string                    `json:"alias" gorm:"uniqueIndex"` | ||||
| 	Content     *string                   `json:"content"` | ||||
| 	Type       string            `json:"type"` | ||||
| 	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"` | ||||
| 	Attachments datatypes.JSONSlice[uint] `json:"attachments"` | ||||
| 	ReplyID    *uint             `json:"reply_id"` | ||||
| 	RepostID   *uint             `json:"repost_id"` | ||||
| 	RealmID    *uint             `json:"realm_id"` | ||||
| @@ -26,9 +30,24 @@ type Post struct { | ||||
|  | ||||
| 	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"` | ||||
|  | ||||
| 	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"` | ||||
| } | ||||
|   | ||||
| @@ -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"` | ||||
| } | ||||
|   | ||||
| @@ -2,164 +2,73 @@ 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" | ||||
| 	jsoniter "github.com/json-iterator/go" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| 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 { | ||||
| 	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"` | ||||
| 		Title       string            `json:"title" validate:"required"` | ||||
| 		Description string            `json:"description"` | ||||
| 		Content     string            `json:"content"` | ||||
| 		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"` | ||||
| 		Attachments []uint            `json:"attachments"` | ||||
| 		IsDraft     bool              `json:"is_draft"` | ||||
| 		PublishedAt    *time.Time        `json:"published_at"` | ||||
| 		RealmAlias  string            `json:"realm"` | ||||
| 		PublishedUntil *time.Time        `json:"published_until"` | ||||
| 		IsDraft        bool              `json:"is_draft"` | ||||
| 		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, | ||||
| 	body := models.PostArticleBody{ | ||||
| 		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 { | ||||
| 	var bodyMapping map[string]any | ||||
| 	rawBody, _ := jsoniter.Marshal(body) | ||||
| 	_ = jsoniter.Unmarshal(rawBody, &bodyMapping) | ||||
|  | ||||
| 	item := models.Post{ | ||||
| 		Type:           models.PostTypeArticle, | ||||
| 		Body:           bodyMapping, | ||||
| 		Language:       services.DetectLanguage(data.Content), | ||||
| 		Tags:           data.Tags, | ||||
| 		Categories:     data.Categories, | ||||
| 		IsDraft:        data.IsDraft, | ||||
| 		PublishedAt:    data.PublishedAt, | ||||
| 		PublishedUntil: data.PublishedUntil, | ||||
| 		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("you aren't a part of related realm: %v", err)) | ||||
| 		} 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.NewArticle(user, item) | ||||
| 	item, err := services.NewPost(user, item) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
| @@ -168,114 +77,57 @@ func createArticle(c *fiber.Ctx) error { | ||||
| } | ||||
|  | ||||
| func editArticle(c *fiber.Ctx) error { | ||||
| 	id, _ := c.ParamsInt("articleId", 0) | ||||
| 	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"` | ||||
| 		Title       string            `json:"title"` | ||||
| 		Description string            `json:"description"` | ||||
| 		Content     string            `json:"content"` | ||||
| 		IsDraft     bool              `json:"is_draft"` | ||||
| 		PublishedAt *time.Time        `json:"published_at"` | ||||
| 		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"` | ||||
| 		Attachments []uint            `json:"attachments"` | ||||
| 		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 { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var item models.Article | ||||
| 	if err := database.C.Where(models.Article{ | ||||
| 	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)) | ||||
| 		} | ||||
| 	body := models.PostArticleBody{ | ||||
| 		Title:       data.Title, | ||||
| 		Content:     data.Content, | ||||
| 		Attachments: data.Attachments, | ||||
| 	} | ||||
|  | ||||
| 	item.Alias = data.Alias | ||||
| 	item.Title = data.Title | ||||
| 	item.Description = data.Description | ||||
| 	item.Content = data.Content | ||||
| 	item.IsDraft = data.IsDraft | ||||
| 	item.PublishedAt = data.PublishedAt | ||||
| 	var bodyMapping map[string]any | ||||
| 	rawBody, _ := jsoniter.Marshal(body) | ||||
| 	_ = jsoniter.Unmarshal(rawBody, &bodyMapping) | ||||
|  | ||||
| 	item.Body = bodyMapping | ||||
| 	item.Language = services.DetectLanguage(data.Content) | ||||
| 	item.Tags = data.Tags | ||||
| 	item.Categories = data.Categories | ||||
| 	item.Attachments = data.Attachments | ||||
| 	item.IsDraft = data.IsDraft | ||||
| 	item.PublishedAt = data.PublishedAt | ||||
| 	item.PublishedUntil = data.PublishedUntil | ||||
|  | ||||
| 	if item, err := services.EditArticle(item); err != nil { | ||||
| 	if item, err := services.EditPost(item); err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} else { | ||||
| 		return c.JSON(item) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func deleteArticle(c *fiber.Ctx) error { | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,173 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"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) | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 		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) | ||||
|  | ||||
| 		return FeedRecord{ | ||||
| 			Type:      t, | ||||
| 			Data:      result, | ||||
| 			CreatedAt: createdAt, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, post := range postItems { | ||||
| 		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, | ||||
| 		"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) | ||||
| 	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) | ||||
|  | ||||
| 		return FeedRecord{ | ||||
| 			Type:      t, | ||||
| 			Data:      result, | ||||
| 			CreatedAt: createdAt, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, post := range postItems { | ||||
| 		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, | ||||
| 		"data":  feed, | ||||
| 	}) | ||||
| } | ||||
| @@ -10,37 +10,28 @@ 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") | ||||
| 		stories := api.Group("/stories").Name("Story API") | ||||
| 		{ | ||||
| 			drafts.Get("/", listDraftMixed) | ||||
| 			drafts.Get("/posts", listDraftPost) | ||||
| 			drafts.Get("/articles", listDraftArticle) | ||||
| 			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("/drafts", listDraftPost) | ||||
| 			posts.Get("/:postId", getPost) | ||||
| 			posts.Post("/:postId/react", reactPost) | ||||
| 			posts.Delete("/:postId", deletePost) | ||||
|  | ||||
| 			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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										153
									
								
								pkg/internal/server/api/stories_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								pkg/internal/server/api/stories_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| 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"` | ||||
| 		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 { | ||||
| 		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{ | ||||
| 		Type:           models.PostTypeStory, | ||||
| 		Body:           bodyMapping, | ||||
| 		Language:       services.DetectLanguage(data.Content), | ||||
| 		Tags:           data.Tags, | ||||
| 		Categories:     data.Categories, | ||||
| 		PublishedAt:    data.PublishedAt, | ||||
| 		PublishedUntil: data.PublishedUntil, | ||||
| 		IsDraft:        data.IsDraft, | ||||
| 		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"` | ||||
| 		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 { | ||||
| 		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.Language = services.DetectLanguage(data.Content) | ||||
| 	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()) | ||||
| 	} else { | ||||
| 		return c.JSON(item) | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -5,14 +5,12 @@ import ( | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func DetectLanguage(content *string) string { | ||||
| 	if content != nil { | ||||
| func DetectLanguage(content string) string { | ||||
| 	detector := lingua.NewLanguageDetectorBuilder(). | ||||
| 		FromLanguages(lingua.AllLanguages()...). | ||||
| 		Build() | ||||
| 		if lang, ok := detector.DetectLanguageOf(*content); ok { | ||||
| 	if lang, ok := detector.DetectLanguageOf(content); ok { | ||||
| 		return strings.ToLower(lang.String()) | ||||
| 	} | ||||
| 	} | ||||
| 	return "unknown" | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
| @@ -55,33 +57,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()) | ||||
| @@ -148,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"). | ||||
| @@ -243,7 +218,9 @@ func EnsurePostCategoriesAndTags(item models.Post) (models.Post, error) { | ||||
| } | ||||
|  | ||||
| func NewPost(user models.Account, item models.Post) (models.Post, error) { | ||||
| 	item.Language = DetectLanguage(item.Content) | ||||
| 	if !item.IsDraft && item.PublishedAt == nil { | ||||
| 		item.PublishedAt = lo.ToPtr(time.Now()) | ||||
| 	} | ||||
|  | ||||
| 	item, err := EnsurePostCategoriesAndTags(item) | ||||
| 	if err != nil { | ||||
| @@ -272,7 +249,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 +263,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 +288,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...") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user