diff --git a/pkg/internal/http/api/index.go b/pkg/internal/http/api/index.go index 476dc78..9fe940a 100644 --- a/pkg/internal/http/api/index.go +++ b/pkg/internal/http/api/index.go @@ -42,6 +42,11 @@ func MapAPIs(app *fiber.App, baseURL string) { questions.Put("/:postId", editQuestion) questions.Put("/:postId/answer", selectQuestionAnswer) } + videos := api.Group("/videos").Name("Video API") + { + videos.Post("/", createVideo) + videos.Put("/:postId", editVideo) + } posts := api.Group("/posts").Name("Posts API") { diff --git a/pkg/internal/http/api/videos_api.go b/pkg/internal/http/api/videos_api.go new file mode 100644 index 0000000..4299094 --- /dev/null +++ b/pkg/internal/http/api/videos_api.go @@ -0,0 +1,210 @@ +package api + +import ( + "git.solsynth.dev/hypernet/interactive/pkg/internal/database" + "git.solsynth.dev/hypernet/interactive/pkg/internal/gap" + "git.solsynth.dev/hypernet/interactive/pkg/internal/http/exts" + "git.solsynth.dev/hypernet/interactive/pkg/internal/models" + "git.solsynth.dev/hypernet/interactive/pkg/internal/services" + "git.solsynth.dev/hypernet/nexus/pkg/nex/cruda" + "git.solsynth.dev/hypernet/nexus/pkg/nex/sec" + "git.solsynth.dev/hypernet/passport/pkg/authkit" + authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models" + "github.com/gofiber/fiber/v2" + jsoniter "github.com/json-iterator/go" + "github.com/samber/lo" + "strconv" + "time" +) + +func createVideo(c *fiber.Ctx) error { + if err := sec.EnsureGrantedPerm(c, "CreatePosts", true); err != nil { + return err + } + user := c.Locals("user").(authm.Account) + + var data struct { + Publisher uint `json:"publisher"` + Video string `json:"video" validate:"required"` + Alias *string `json:"alias"` + Title string `json:"title" validate:"required"` + Description *string `json:"description" validate:"max=4096"` + Location *string `json:"location"` + Thumbnail *string `json:"thumbnail"` + Subtitles map[string]string `json:"subtitles"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` + VisibleUsers []uint `json:"visible_users_list"` + InvisibleUsers []uint `json:"invisible_users_list"` + Visibility *int8 `json:"visibility"` + IsDraft bool `json:"is_draft"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + publisher, err := services.GetPublisher(data.Publisher, user.ID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + body := models.PostVideoBody{ + Thumbnail: data.Thumbnail, + Video: data.Video, + Title: data.Title, + Description: data.Description, + Location: data.Location, + Subtitles: data.Subtitles, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item := models.Post{ + Alias: data.Alias, + Type: models.PostTypeVideo, + Body: bodyMapping, + Language: services.DetectLanguage(data.Title), + Tags: data.Tags, + Categories: data.Categories, + PublishedAt: data.PublishedAt, + PublishedUntil: data.PublishedUntil, + IsDraft: data.IsDraft, + VisibleUsers: data.VisibleUsers, + InvisibleUsers: data.InvisibleUsers, + PublisherID: publisher.ID, + } + + if item.PublishedAt == nil { + item.PublishedAt = lo.ToPtr(time.Now()) + } + + if data.Visibility != nil { + item.Visibility = *data.Visibility + } else { + item.Visibility = models.PostVisibilityAll + } + + item, err = services.NewPost(publisher, item) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + _ = authkit.AddEventExt( + gap.Nx, + "posts.new", + strconv.Itoa(int(item.ID)), + c, + ) + } + + return c.JSON(item) +} + +func editVideo(c *fiber.Ctx) error { + id, _ := c.ParamsInt("postId", 0) + if err := sec.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(authm.Account) + + var data struct { + Publisher uint `json:"publisher"` + Video string `json:"video" validate:"required"` + Alias *string `json:"alias"` + Title string `json:"title" validate:"required"` + Description *string `json:"description" validate:"max=4096"` + Location *string `json:"location"` + Thumbnail *string `json:"thumbnail"` + Subtitles map[string]string `json:"subtitles"` + Tags []models.Tag `json:"tags"` + Categories []models.Category `json:"categories"` + PublishedAt *time.Time `json:"published_at"` + PublishedUntil *time.Time `json:"published_until"` + VisibleUsers []uint `json:"visible_users_list"` + InvisibleUsers []uint `json:"invisible_users_list"` + Visibility *int8 `json:"visibility"` + IsDraft bool `json:"is_draft"` + } + + if err := exts.BindAndValidate(c, &data); err != nil { + return err + } + + publisher, err := services.GetPublisher(data.Publisher, user.ID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + var item models.Post + if err := database.C.Where(models.Post{ + BaseModel: cruda.BaseModel{ID: uint(id)}, + PublisherID: publisher.ID, + Type: models.PostTypeVideo, + }).Preload("Publisher").First(&item).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + if item.LockedAt != nil { + return fiber.NewError(fiber.StatusForbidden, "post was locked") + } + + if !item.IsDraft && !data.IsDraft { + item.EditedAt = lo.ToPtr(time.Now()) + } + + if item.IsDraft && !data.IsDraft && data.PublishedAt == nil { + item.PublishedAt = lo.ToPtr(time.Now()) + } else { + item.PublishedAt = data.PublishedAt + } + + body := models.PostVideoBody{ + Thumbnail: data.Thumbnail, + Video: data.Video, + Title: data.Title, + Description: data.Description, + Location: data.Location, + Subtitles: data.Subtitles, + } + + var bodyMapping map[string]any + rawBody, _ := jsoniter.Marshal(body) + _ = jsoniter.Unmarshal(rawBody, &bodyMapping) + + item.Alias = data.Alias + item.Body = bodyMapping + item.Language = services.DetectLanguage(data.Title) + item.Tags = data.Tags + item.Categories = data.Categories + item.IsDraft = data.IsDraft + item.PublishedUntil = data.PublishedUntil + item.VisibleUsers = data.VisibleUsers + item.InvisibleUsers = data.InvisibleUsers + + // Preload publisher data + item.Publisher = publisher + + if item.PublishedAt == nil { + item.PublishedAt = data.PublishedAt + } + if data.Visibility != nil { + item.Visibility = *data.Visibility + } + + if item, err = services.EditPost(item); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + _ = authkit.AddEventExt( + gap.Nx, + "posts.edit", + strconv.Itoa(int(item.ID)), + c, + ) + } + + return c.JSON(item) +} diff --git a/pkg/internal/models/posts.go b/pkg/internal/models/posts.go index c40879c..f58238c 100644 --- a/pkg/internal/models/posts.go +++ b/pkg/internal/models/posts.go @@ -13,6 +13,7 @@ const ( PostTypeStory = "story" PostTypeArticle = "article" PostTypeQuestion = "question" + PostTypeVideo = "video" ) type PostVisibilityLevel = int8 @@ -88,6 +89,17 @@ type PostQuestionBody struct { Reward float64 `json:"reward"` } +type PostVideoBody struct { + Thumbnail *string `json:"thumbnail"` + Title string `json:"title"` + Description *string `json:"description"` + Location *string `json:"location"` + Video string `json:"video"` + IsLive bool `json:"is_live"` + IsLiveEnded bool `json:"is_live_ended"` + Subtitles map[string]string `json:"subtitles"` +} + type PostInsight struct { cruda.BaseModel