diff --git a/pkg/internal/models/posts.go b/pkg/internal/models/posts.go index 191f5a5..f15eb66 100644 --- a/pkg/internal/models/posts.go +++ b/pkg/internal/models/posts.go @@ -9,20 +9,18 @@ import ( type Post struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` - Content *string `json:"content"` - Language string `json:"language"` - Tags []Tag `json:"tags" gorm:"many2many:post_tags"` - Categories []Category `json:"categories" gorm:"many2many:post_categories"` - Reactions []Reaction `json:"reactions"` - Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"` - Attachments datatypes.JSONSlice[uint] `json:"attachments"` - ReplyID *uint `json:"reply_id"` - RepostID *uint `json:"repost_id"` - RealmID *uint `json:"realm_id"` - ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"` - RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"` - Realm *Realm `json:"realm"` + Body datatypes.JSONMap `json:"body"` + Language string `json:"language"` + Tags []Tag `json:"tags" gorm:"many2many:post_tags"` + Categories []Category `json:"categories" gorm:"many2many:post_categories"` + Reactions []Reaction `json:"reactions"` + Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"` + ReplyID *uint `json:"reply_id"` + RepostID *uint `json:"repost_id"` + RealmID *uint `json:"realm_id"` + ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"` + RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"` + Realm *Realm `json:"realm"` IsDraft bool `json:"is_draft"` PublishedAt *time.Time `json:"published_at"` @@ -32,3 +30,17 @@ type Post struct { Metric PostMetric `json:"metric" gorm:"-"` } + +type PostStoryBody struct { + Title *string `json:"title"` + Content string `json:"content"` + Location *string `json:"location"` + Attachments []uint `json:"attachments"` +} + +type PostArticleBody struct { + Title string `json:"title"` + Description *string `json:"description"` + Content string `json:"content"` + Attachments []uint `json:"attachments"` +} diff --git a/pkg/internal/server/api/articles_api.go b/pkg/internal/server/api/articles_api.go new file mode 100644 index 0000000..5d87551 --- /dev/null +++ b/pkg/internal/server/api/articles_api.go @@ -0,0 +1,126 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" + "time" +) + +func createArticle(c *fiber.Ctx) error { + if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + PublishedAt *time.Time `json:"published_at"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + body := models.PostArticleBody{ + Title: data.Title, + Description: data.Description, + Content: data.Content, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item := models.Post{ + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + IsDraft: data.IsDraft, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + } + + if data.RealmAlias != nil { + if realm, err := services.GetRealmWithAlias(*data.RealmAlias); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if _, err = services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to post in the realm, access denied: %v", err)) + } else { + item.RealmID = &realm.ID + } + } + + item, err := services.NewPost(user, item) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(item) +} + +func editArticle(c *fiber.Ctx) error { + id, _ := c.ParamsInt("postId", 0) + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title string `json:"title" validate:"required,max=1024"` + Description *string `json:"description" validate:"max=2048"` + Content string `json:"content" validate:"required"` + Attachments []uint `json:"attachments"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + var item models.Post + if err := database.C.Where(models.Post{ + BaseModel: models.BaseModel{ID: uint(id)}, + AuthorID: user.ID, + }).First(&item).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + body := models.PostArticleBody{ + Title: data.Title, + Content: data.Content, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item.Body = bodyMapping + item.IsDraft = data.IsDraft + item.PublishedAt = data.PublishedAt + item.Tags = data.Tags + item.Categories = data.Categories + + if item, err := services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) + } +} diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 21db98c..5760e24 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -18,13 +18,22 @@ func MapAPIs(app *fiber.App, baseURL string) { drafts.Get("/posts", listDraftPost) } + stories := api.Group("/stories").Name("Story API") + { + stories.Post("/", createStory) + stories.Put("/:postId", editStory) + } + articles := api.Group("/articles").Name("Article API") + { + articles.Post("/", createArticle) + articles.Put("/:articleId", editArticle) + } + posts := api.Group("/posts").Name("Posts API") { posts.Get("/", listPost) - posts.Get("/:post", getPost) - posts.Post("/", createPost) - posts.Post("/:post/react", reactPost) - posts.Put("/:postId", editPost) + posts.Get("/:postId", getPost) + posts.Post("/:postId/react", reactPost) posts.Delete("/:postId", deletePost) posts.Get("/:post/replies", listPostReplies) diff --git a/pkg/internal/server/api/posts_api.go b/pkg/internal/server/api/posts_api.go index 8662535..6ceaac6 100644 --- a/pkg/internal/server/api/posts_api.go +++ b/pkg/internal/server/api/posts_api.go @@ -2,23 +2,19 @@ package api import ( "fmt" - "strings" - "time" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" "github.com/samber/lo" ) func getPost(c *fiber.Ctx) error { - alias := c.Params("post") + id, _ := c.ParamsInt("postId") - item, err := services.GetPostWithAlias(services.FilterPostDraft(database.C), alias) + item, err := services.GetPost(services.FilterPostDraft(database.C), uint(id)) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -108,133 +104,6 @@ func listDraftPost(c *fiber.Ctx) error { }) } -func createPost(c *fiber.Ctx) error { - if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Content string `json:"content" validate:"required,max=4096"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - RealmAlias string `json:"realm"` - ReplyTo *uint `json:"reply_to"` - RepostTo *uint `json:"repost_to"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } else if len(data.Alias) == 0 { - data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item := models.Post{ - Alias: data.Alias, - Content: &data.Content, - Tags: data.Tags, - Categories: data.Categories, - Attachments: data.Attachments, - IsDraft: data.IsDraft, - PublishedAt: data.PublishedAt, - AuthorID: user.ID, - } - - if data.ReplyTo != nil { - var replyTo models.Post - if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) - } else { - item.ReplyID = &replyTo.ID - } - } - if data.RepostTo != nil { - var repostTo models.Post - if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) - } else { - item.RepostID = &repostTo.ID - } - } - - if len(data.RealmAlias) > 0 { - if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err)) - } else { - item.RealmID = &realm.ID - } - } - - item, err := services.NewPost(user, item) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(item) -} - -func editPost(c *fiber.Ctx) error { - id, _ := c.ParamsInt("postId", 0) - if err := gap.H.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Alias string `json:"alias"` - Content string `json:"content" validate:"required,max=4096"` - IsDraft bool `json:"is_draft"` - PublishedAt *time.Time `json:"published_at"` - Tags []models.Tag `json:"tags"` - Categories []models.Category `json:"categories"` - Attachments []uint `json:"attachments"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } - - var item models.Post - if err := database.C.Where(models.Post{ - BaseModel: models.BaseModel{ID: uint(id)}, - AuthorID: user.ID, - }).First(&item).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - for _, attachment := range data.Attachments { - if !services.CheckAttachmentByIDExists(attachment, "i.attachment") { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment)) - } - } - - item.Content = &data.Content - item.Alias = data.Alias - item.IsDraft = data.IsDraft - item.PublishedAt = data.PublishedAt - item.Tags = data.Tags - item.Categories = data.Categories - item.Attachments = data.Attachments - - if item, err := services.EditPost(item); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } else { - return c.JSON(item) - } -} - func deletePost(c *fiber.Ctx) error { if err := gap.H.EnsureAuthenticated(c); err != nil { return err diff --git a/pkg/internal/server/api/stories_api.go b/pkg/internal/server/api/stories_api.go new file mode 100644 index 0000000..df35f48 --- /dev/null +++ b/pkg/internal/server/api/stories_api.go @@ -0,0 +1,146 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" + "time" +) + +func createStory(c *fiber.Ctx) error { + if err := gap.H.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + IsDraft bool `json:"is_draft"` + RealmAlias *string `json:"realm"` + ReplyTo *uint `json:"reply_to"` + RepostTo *uint `json:"repost_to"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + body := models.PostStoryBody{ + Title: data.Title, + Content: data.Content, + Location: data.Location, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item := models.Post{ + Body: bodyMapping, + Tags: data.Tags, + Categories: data.Categories, + IsDraft: data.IsDraft, + PublishedAt: data.PublishedAt, + AuthorID: user.ID, + } + + if data.ReplyTo != nil { + var replyTo models.Post + if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) + } else { + item.ReplyID = &replyTo.ID + } + } + if data.RepostTo != nil { + var repostTo models.Post + if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err)) + } else { + item.RepostID = &repostTo.ID + } + } + + if data.RealmAlias != nil { + if realm, err := services.GetRealmWithAlias(*data.RealmAlias); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if _, err = services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to post in the realm, access denied: %v", err)) + } else { + item.RealmID = &realm.ID + } + } + + item, err := services.NewPost(user, item) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(item) +} + +func editStory(c *fiber.Ctx) error { + id, _ := c.ParamsInt("postId", 0) + if err := gap.H.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + + var data struct { + Title *string `json:"title" validate:"max=1024"` + Content string `json:"content" validate:"required,max=4096"` + Location *string `json:"location" validate:"max=2048"` + Attachments []uint `json:"attachments"` + IsDraft bool `json:"is_draft"` + PublishedAt *time.Time `json:"published_at"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + var item models.Post + if err := database.C.Where(models.Post{ + BaseModel: models.BaseModel{ID: uint(id)}, + AuthorID: user.ID, + }).First(&item).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + body := models.PostStoryBody{ + Title: data.Title, + Content: data.Content, + Location: data.Location, + Attachments: data.Attachments, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item.Body = bodyMapping + item.IsDraft = data.IsDraft + item.PublishedAt = data.PublishedAt + item.Tags = data.Tags + item.Categories = data.Categories + + if item, err := services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) + } +} diff --git a/pkg/internal/services/attachments.go b/pkg/internal/services/attachments.go deleted file mode 100644 index 6ab544a..0000000 --- a/pkg/internal/services/attachments.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "context" - "git.solsynth.dev/hydrogen/dealer/pkg/hyper" - "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" - "git.solsynth.dev/hydrogen/paperclip/pkg/proto" - "github.com/samber/lo" -) - -func CheckAttachmentByIDExists(id uint, usage string) bool { - pc, err := gap.H.GetServiceGrpcConn(hyper.ServiceTypeFileProvider) - if err != nil { - return false - } - _, err = proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{ - Id: lo.ToPtr(uint64(id)), - Usage: &usage, - }) - return err == nil -} diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index f56d0c8..998f3a2 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -55,33 +55,6 @@ func FilterPostDraft(tx *gorm.DB) *gorm.DB { return tx.Where("is_draft = ? OR is_draft IS NULL", false) } -func GetPostWithAlias(tx *gorm.DB, alias string, ignoreLimitation ...bool) (models.Post, error) { - if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { - tx = FilterPostWithPublishedAt(tx, time.Now()) - } - - var item models.Post - if err := tx. - Where("alias = ?", alias). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - Preload("ReplyTo"). - Preload("ReplyTo.Author"). - Preload("ReplyTo.Tags"). - Preload("ReplyTo.Categories"). - Preload("RepostTo"). - Preload("RepostTo.Author"). - Preload("RepostTo.Tags"). - Preload("RepostTo.Categories"). - First(&item).Error; err != nil { - return item, err - } - - return item, nil -} - func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error) { if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { tx = FilterPostWithPublishedAt(tx, time.Now()) @@ -243,8 +216,6 @@ func EnsurePostCategoriesAndTags(item models.Post) (models.Post, error) { } func NewPost(user models.Account, item models.Post) (models.Post, error) { - item.Language = DetectLanguage(item.Content) - item, err := EnsurePostCategoriesAndTags(item) if err != nil { return item, err @@ -272,7 +243,7 @@ func NewPost(user models.Account, item models.Post) (models.Post, error) { err = NotifyPosterAccount( op.Author, "Post got replied", - fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), + fmt.Sprintf("%s (%s) replied your post.", user.Nick, user.Name), lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), ) if err != nil { @@ -286,7 +257,6 @@ func NewPost(user models.Account, item models.Post) (models.Post, error) { } func EditPost(item models.Post) (models.Post, error) { - item.Language = DetectLanguage(item.Content) item, err := EnsurePostCategoriesAndTags(item) if err != nil { return item, err @@ -312,9 +282,9 @@ func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reac if op.Author.ID != user.ID { err = NotifyPosterAccount( op.Author, - "Post got replied", - fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias), - lo.ToPtr(fmt.Sprintf("%s replied you", user.Nick)), + "Post got reacted", + fmt.Sprintf("%s (%s) reacted your post a %s.", user.Nick, user.Name, reaction.Symbol), + lo.ToPtr(fmt.Sprintf("%s reacted you", user.Nick)), ) if err != nil { log.Error().Err(err).Msg("An error occurred when notifying user...")