♻️ 一切尽在帖子表 #4
| @@ -11,7 +11,6 @@ var AutoMaintainRange = []any{ | |||||||
| 	&models.Category{}, | 	&models.Category{}, | ||||||
| 	&models.Tag{}, | 	&models.Tag{}, | ||||||
| 	&models.Post{}, | 	&models.Post{}, | ||||||
| 	&models.Article{}, |  | ||||||
| 	&models.Reaction{}, | 	&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 { | type Tag struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string    `json:"alias" gorm:"uniqueIndex" validate:"lowercase"` | 	Alias       string `json:"alias" gorm:"uniqueIndex" validate:"lowercase"` | ||||||
| 	Name        string    `json:"name"` | 	Name        string `json:"name"` | ||||||
| 	Description string    `json:"description"` | 	Description string `json:"description"` | ||||||
| 	Posts       []Post    `json:"posts" gorm:"many2many:post_tags"` | 	Posts       []Post `json:"posts" gorm:"many2many:post_tags"` | ||||||
| 	Articles    []Article `json:"articles" gorm:"many2many:article_tags"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type Category struct { | type Category struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string    `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum"` | 	Alias       string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum"` | ||||||
| 	Name        string    `json:"name"` | 	Name        string `json:"name"` | ||||||
| 	Description string    `json:"description"` | 	Description string `json:"description"` | ||||||
| 	Posts       []Post    `json:"posts" gorm:"many2many:post_categories"` | 	Posts       []Post `json:"posts" gorm:"many2many:post_categories"` | ||||||
| 	Articles    []Article `json:"articles" gorm:"many2many:article_categories"` |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,29 +6,48 @@ import ( | |||||||
| 	"gorm.io/datatypes" | 	"gorm.io/datatypes" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	PostTypeStory   = "story" | ||||||
|  | 	PostTypeArticle = "article" | ||||||
|  | ) | ||||||
|  |  | ||||||
| type Post struct { | type Post struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string                    `json:"alias" gorm:"uniqueIndex"` | 	Type       string            `json:"type"` | ||||||
| 	Content     *string                   `json:"content"` | 	Body       datatypes.JSONMap `json:"body"` | ||||||
| 	Language    string                    `json:"language"` | 	Language   string            `json:"language"` | ||||||
| 	Tags        []Tag                     `json:"tags" gorm:"many2many:post_tags"` | 	Tags       []Tag             `json:"tags" gorm:"many2many:post_tags"` | ||||||
| 	Categories  []Category                `json:"categories" gorm:"many2many:post_categories"` | 	Categories []Category        `json:"categories" gorm:"many2many:post_categories"` | ||||||
| 	Reactions   []Reaction                `json:"reactions"` | 	Reactions  []Reaction        `json:"reactions"` | ||||||
| 	Replies     []Post                    `json:"replies" gorm:"foreignKey:ReplyID"` | 	Replies    []Post            `json:"replies" gorm:"foreignKey:ReplyID"` | ||||||
| 	Attachments datatypes.JSONSlice[uint] `json:"attachments"` | 	ReplyID    *uint             `json:"reply_id"` | ||||||
| 	ReplyID     *uint                     `json:"reply_id"` | 	RepostID   *uint             `json:"repost_id"` | ||||||
| 	RepostID    *uint                     `json:"repost_id"` | 	RealmID    *uint             `json:"realm_id"` | ||||||
| 	RealmID     *uint                     `json:"realm_id"` | 	ReplyTo    *Post             `json:"reply_to" gorm:"foreignKey:ReplyID"` | ||||||
| 	ReplyTo     *Post                     `json:"reply_to" gorm:"foreignKey:ReplyID"` | 	RepostTo   *Post             `json:"repost_to" gorm:"foreignKey:RepostID"` | ||||||
| 	RepostTo    *Post                     `json:"repost_to" gorm:"foreignKey:RepostID"` | 	Realm      *Realm            `json:"realm"` | ||||||
| 	Realm       *Realm                    `json:"realm"` |  | ||||||
|  |  | ||||||
| 	IsDraft     bool       `json:"is_draft"` | 	IsDraft        bool       `json:"is_draft"` | ||||||
| 	PublishedAt *time.Time `json:"published_at"` | 	PublishedAt    *time.Time `json:"published_at"` | ||||||
|  | 	PublishedUntil *time.Time `json:"published_until"` | ||||||
|  |  | ||||||
| 	AuthorID uint    `json:"author_id"` | 	AuthorID uint    `json:"author_id"` | ||||||
| 	Author   Account `json:"author"` | 	Author   Account `json:"author"` | ||||||
|  |  | ||||||
| 	Metric PostMetric `json:"metric" gorm:"-"` | 	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"` | 	Attitude ReactionAttitude `json:"attitude"` | ||||||
|  |  | ||||||
| 	PostID    *uint `json:"post_id"` | 	PostID    *uint `json:"post_id"` | ||||||
| 	ArticleID *uint `json:"article_id"` |  | ||||||
| 	AccountID uint  `json:"account_id"` | 	AccountID uint  `json:"account_id"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,164 +2,73 @@ package api | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/database" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/database" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/models" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/models" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/services" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/services" | ||||||
| 	"github.com/gofiber/fiber/v2" | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"github.com/google/uuid" | 	jsoniter "github.com/json-iterator/go" | ||||||
| 	"github.com/samber/lo" | 	"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 { | 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 | 		return err | ||||||
| 	} | 	} | ||||||
| 	user := c.Locals("user").(models.Account) | 	user := c.Locals("user").(models.Account) | ||||||
|  |  | ||||||
| 	var data struct { | 	var data struct { | ||||||
| 		Alias       string            `json:"alias"` | 		Title          string            `json:"title" validate:"required,max=1024"` | ||||||
| 		Title       string            `json:"title" validate:"required"` | 		Description    *string           `json:"description" validate:"max=2048"` | ||||||
| 		Description string            `json:"description"` | 		Content        string            `json:"content" validate:"required"` | ||||||
| 		Content     string            `json:"content"` | 		Attachments    []uint            `json:"attachments"` | ||||||
| 		Tags        []models.Tag      `json:"tags"` | 		Tags           []models.Tag      `json:"tags"` | ||||||
| 		Categories  []models.Category `json:"categories"` | 		Categories     []models.Category `json:"categories"` | ||||||
| 		Attachments []uint            `json:"attachments"` | 		PublishedAt    *time.Time        `json:"published_at"` | ||||||
| 		IsDraft     bool              `json:"is_draft"` | 		PublishedUntil *time.Time        `json:"published_until"` | ||||||
| 		PublishedAt *time.Time        `json:"published_at"` | 		IsDraft        bool              `json:"is_draft"` | ||||||
| 		RealmAlias  string            `json:"realm"` | 		RealmAlias     *string           `json:"realm"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := exts.BindAndValidate(c, &data); err != nil { | 	if err := exts.BindAndValidate(c, &data); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} else if len(data.Alias) == 0 { |  | ||||||
| 		data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, attachment := range data.Attachments { | 	body := models.PostArticleBody{ | ||||||
| 		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, | 		Title:       data.Title, | ||||||
| 		Description: data.Description, | 		Description: data.Description, | ||||||
| 		Content:     data.Content, | 		Content:     data.Content, | ||||||
| 		IsDraft:     data.IsDraft, |  | ||||||
| 		PublishedAt: data.PublishedAt, |  | ||||||
| 		AuthorID:    user.ID, |  | ||||||
| 		Tags:        data.Tags, |  | ||||||
| 		Categories:  data.Categories, |  | ||||||
| 		Attachments: data.Attachments, | 		Attachments: data.Attachments, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(data.RealmAlias) > 0 { | 	var bodyMapping map[string]any | ||||||
| 		if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil { | 	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()) | 			return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
| 		} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { | 		} 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)) | 			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to post in the realm, access denied: %v", err)) | ||||||
| 		} else { | 		} else { | ||||||
| 			item.RealmID = &realm.ID | 			item.RealmID = &realm.ID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	item, err := services.NewArticle(user, item) | 	item, err := services.NewPost(user, item) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
| 	} | 	} | ||||||
| @@ -168,114 +77,57 @@ func createArticle(c *fiber.Ctx) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func editArticle(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 { | 	if err := gap.H.EnsureAuthenticated(c); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	user := c.Locals("user").(models.Account) | 	user := c.Locals("user").(models.Account) | ||||||
|  |  | ||||||
| 	var data struct { | 	var data struct { | ||||||
| 		Alias       string            `json:"alias"` | 		Title          string            `json:"title" validate:"required,max=1024"` | ||||||
| 		Title       string            `json:"title"` | 		Description    *string           `json:"description" validate:"max=2048"` | ||||||
| 		Description string            `json:"description"` | 		Content        string            `json:"content" validate:"required"` | ||||||
| 		Content     string            `json:"content"` | 		Attachments    []uint            `json:"attachments"` | ||||||
| 		IsDraft     bool              `json:"is_draft"` | 		Tags           []models.Tag      `json:"tags"` | ||||||
| 		PublishedAt *time.Time        `json:"published_at"` | 		Categories     []models.Category `json:"categories"` | ||||||
| 		Tags        []models.Tag      `json:"tags"` | 		IsDraft        bool              `json:"is_draft"` | ||||||
| 		Categories  []models.Category `json:"categories"` | 		PublishedAt    *time.Time        `json:"published_at"` | ||||||
| 		Attachments []uint            `json:"attachments"` | 		PublishedUntil *time.Time        `json:"published_until"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := exts.BindAndValidate(c, &data); err != nil { | 	if err := exts.BindAndValidate(c, &data); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var item models.Article | 	var item models.Post | ||||||
| 	if err := database.C.Where(models.Article{ | 	if err := database.C.Where(models.Post{ | ||||||
| 		BaseModel: models.BaseModel{ID: uint(id)}, | 		BaseModel: models.BaseModel{ID: uint(id)}, | ||||||
| 		AuthorID:  user.ID, | 		AuthorID:  user.ID, | ||||||
| 	}).First(&item).Error; err != nil { | 	}).First(&item).Error; err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, attachment := range data.Attachments { | 	body := models.PostArticleBody{ | ||||||
| 		if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { | 		Title:       data.Title, | ||||||
| 			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) | 		Content:     data.Content, | ||||||
| 		} | 		Attachments: data.Attachments, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	item.Alias = data.Alias | 	var bodyMapping map[string]any | ||||||
| 	item.Title = data.Title | 	rawBody, _ := jsoniter.Marshal(body) | ||||||
| 	item.Description = data.Description | 	_ = jsoniter.Unmarshal(rawBody, &bodyMapping) | ||||||
| 	item.Content = data.Content |  | ||||||
| 	item.IsDraft = data.IsDraft | 	item.Body = bodyMapping | ||||||
| 	item.PublishedAt = data.PublishedAt | 	item.Language = services.DetectLanguage(data.Content) | ||||||
| 	item.Tags = data.Tags | 	item.Tags = data.Tags | ||||||
| 	item.Categories = data.Categories | 	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()) | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
| 	} else { | 	} else { | ||||||
| 		return c.JSON(item) | 		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/me", getUserinfo) | ||||||
| 		api.Get("/users/:accountId", getOthersInfo) | 		api.Get("/users/:accountId", getOthersInfo) | ||||||
|  |  | ||||||
| 		api.Get("/feed", listFeed) | 		stories := api.Group("/stories").Name("Story API") | ||||||
|  |  | ||||||
| 		drafts := api.Group("/drafts").Name("Draft box API") |  | ||||||
| 		{ | 		{ | ||||||
| 			drafts.Get("/", listDraftMixed) | 			stories.Post("/", createStory) | ||||||
| 			drafts.Get("/posts", listDraftPost) | 			stories.Put("/:postId", editStory) | ||||||
| 			drafts.Get("/articles", listDraftArticle) | 		} | ||||||
|  | 		articles := api.Group("/articles").Name("Article API") | ||||||
|  | 		{ | ||||||
|  | 			articles.Post("/", createArticle) | ||||||
|  | 			articles.Put("/:articleId", editArticle) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		posts := api.Group("/posts").Name("Posts API") | 		posts := api.Group("/posts").Name("Posts API") | ||||||
| 		{ | 		{ | ||||||
| 			posts.Get("/", listPost) | 			posts.Get("/", listPost) | ||||||
| 			posts.Get("/:post", getPost) | 			posts.Get("/drafts", listDraftPost) | ||||||
| 			posts.Post("/", createPost) | 			posts.Get("/:postId", getPost) | ||||||
| 			posts.Post("/:post/react", reactPost) | 			posts.Post("/:postId/react", reactPost) | ||||||
| 			posts.Put("/:postId", editPost) |  | ||||||
| 			posts.Delete("/:postId", deletePost) | 			posts.Delete("/:postId", deletePost) | ||||||
|  |  | ||||||
| 			posts.Get("/:post/replies", listPostReplies) | 			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.Get("/categories", listCategories) | ||||||
| 		api.Post("/categories", newCategory) | 		api.Post("/categories", newCategory) | ||||||
| 		api.Put("/categories/:categoryId", editCategory) | 		api.Put("/categories/:categoryId", editCategory) | ||||||
|   | |||||||
| @@ -2,23 +2,19 @@ package api | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/database" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/database" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/models" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/models" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" | ||||||
| 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/services" | 	"git.solsynth.dev/hydrogen/interactive/pkg/internal/services" | ||||||
| 	"github.com/gofiber/fiber/v2" | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"github.com/google/uuid" |  | ||||||
| 	"github.com/samber/lo" | 	"github.com/samber/lo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func getPost(c *fiber.Ctx) error { | 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 { | 	if err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | 		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 { | func deletePost(c *fiber.Ctx) error { | ||||||
| 	if err := gap.H.EnsureAuthenticated(c); err != nil { | 	if err := gap.H.EnsureAuthenticated(c); err != nil { | ||||||
| 		return err | 		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" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func DetectLanguage(content *string) string { | func DetectLanguage(content string) string { | ||||||
| 	if content != nil { | 	detector := lingua.NewLanguageDetectorBuilder(). | ||||||
| 		detector := lingua.NewLanguageDetectorBuilder(). | 		FromLanguages(lingua.AllLanguages()...). | ||||||
| 			FromLanguages(lingua.AllLanguages()...). | 		Build() | ||||||
| 			Build() | 	if lang, ok := detector.DetectLanguageOf(content); ok { | ||||||
| 		if lang, ok := detector.DetectLanguageOf(*content); ok { | 		return strings.ToLower(lang.String()) | ||||||
| 			return strings.ToLower(lang.String()) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return "unknown" | 	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 { | 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 { | 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) | 	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) { | func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error) { | ||||||
| 	if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { | 	if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { | ||||||
| 		tx = FilterPostWithPublishedAt(tx, time.Now()) | 		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 | 	var items []*models.Post | ||||||
| 	if err := tx. | 	if err := tx. | ||||||
| 		Limit(take).Offset(offset). | 		Limit(take).Offset(offset). | ||||||
| 		Order("created_at DESC"). | 		Order("published_at DESC"). | ||||||
| 		Preload("Tags"). | 		Preload("Tags"). | ||||||
| 		Preload("Categories"). | 		Preload("Categories"). | ||||||
| 		Preload("Realm"). | 		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) { | 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) | 	item, err := EnsurePostCategoriesAndTags(item) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -272,7 +249,7 @@ func NewPost(user models.Account, item models.Post) (models.Post, error) { | |||||||
| 				err = NotifyPosterAccount( | 				err = NotifyPosterAccount( | ||||||
| 					op.Author, | 					op.Author, | ||||||
| 					"Post got replied", | 					"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)), | 					lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), | ||||||
| 				) | 				) | ||||||
| 				if err != nil { | 				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) { | func EditPost(item models.Post) (models.Post, error) { | ||||||
| 	item.Language = DetectLanguage(item.Content) |  | ||||||
| 	item, err := EnsurePostCategoriesAndTags(item) | 	item, err := EnsurePostCategoriesAndTags(item) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return item, err | 		return item, err | ||||||
| @@ -312,9 +288,9 @@ func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reac | |||||||
| 				if op.Author.ID != user.ID { | 				if op.Author.ID != user.ID { | ||||||
| 					err = NotifyPosterAccount( | 					err = NotifyPosterAccount( | ||||||
| 						op.Author, | 						op.Author, | ||||||
| 						"Post got replied", | 						"Post got reacted", | ||||||
| 						fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), | 						fmt.Sprintf("%s (%s) reacted your post a %s.", user.Nick, user.Name, reaction.Symbol), | ||||||
| 						lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), | 						lo.ToPtr(fmt.Sprintf("%s reacted you", user.Nick)), | ||||||
| 					) | 					) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						log.Error().Err(err).Msg("An error occurred when notifying user...") | 						log.Error().Err(err).Msg("An error occurred when notifying user...") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user