164 lines
3.9 KiB
Go
164 lines
3.9 KiB
Go
package entity
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
uploadentity "spark-server/internal/module/upload/entity"
|
|
)
|
|
|
|
const maxContentBytes = 32 << 10
|
|
|
|
// Content is the persisted JSON for images/video. Empty object for text-only posts.
|
|
type Content struct {
|
|
Images []*Image `json:"images,omitempty"`
|
|
Video *Video `json:"video,omitempty"`
|
|
}
|
|
|
|
// Image matches chat image JSON field names so clients reuse the same decoder.
|
|
type Image struct {
|
|
Key string `json:"key"`
|
|
Mime string `json:"mime,omitempty"`
|
|
Width int `json:"w,omitempty"`
|
|
Height int `json:"h,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
ThumbKey string `json:"thumb_key,omitempty"`
|
|
OrigKey string `json:"orig_key,omitempty"`
|
|
}
|
|
|
|
// Video matches chat video JSON field names.
|
|
type Video struct {
|
|
Key string `json:"key"`
|
|
Mime string `json:"mime,omitempty"`
|
|
Width int `json:"w,omitempty"`
|
|
Height int `json:"h,omitempty"`
|
|
DurationMS int `json:"duration_ms,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
CoverKey string `json:"cover_key,omitempty"`
|
|
}
|
|
|
|
func (c *Content) Marshal() ([]byte, error) {
|
|
if c == nil {
|
|
c = &Content{}
|
|
}
|
|
if err := c.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(c); err != nil {
|
|
return nil, fmt.Errorf("moment content: %w", err)
|
|
}
|
|
b := bytes.TrimSuffix(buf.Bytes(), []byte{'\n'})
|
|
if len(b) > maxContentBytes {
|
|
return nil, fmt.Errorf("moment content: too large")
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func UnmarshalContent(raw []byte) (*Content, error) {
|
|
if len(raw) == 0 {
|
|
return &Content{}, nil
|
|
}
|
|
var c Content
|
|
if err := json.Unmarshal(raw, &c); err != nil {
|
|
return nil, fmt.Errorf("moment content: %w", err)
|
|
}
|
|
if err := c.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func (c *Content) Validate() error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
if len(c.Images) > 0 && c.Video != nil {
|
|
return fmt.Errorf("moment content: image and video")
|
|
}
|
|
if len(c.Images) > MaxImages {
|
|
return fmt.Errorf("moment content: too many images")
|
|
}
|
|
for _, img := range c.Images {
|
|
if err := img.validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.Video != nil {
|
|
return c.Video.validate()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Content) Kind() Kind {
|
|
if c != nil && c.Video != nil {
|
|
return KindVideo
|
|
}
|
|
if c != nil && len(c.Images) > 0 {
|
|
return KindImage
|
|
}
|
|
return KindText
|
|
}
|
|
|
|
func (c *Content) ObjectKeys() []string {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 12)
|
|
for _, img := range c.Images {
|
|
out = appendKeys(out, img.Key, img.ThumbKey, img.OrigKey)
|
|
}
|
|
if c.Video != nil {
|
|
out = appendKeys(out, c.Video.Key, c.Video.CoverKey)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (img *Image) validate() error {
|
|
if img == nil || !momentKey(img.Key, uploadentity.PrefixMomentImage) {
|
|
return fmt.Errorf("moment content: image key")
|
|
}
|
|
if img.ThumbKey != "" && !momentKey(img.ThumbKey, uploadentity.PrefixMomentImage) {
|
|
return fmt.Errorf("moment content: thumb")
|
|
}
|
|
if img.OrigKey != "" && !momentKey(img.OrigKey, uploadentity.PrefixMomentImage) {
|
|
return fmt.Errorf("moment content: orig")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *Video) validate() error {
|
|
if v == nil || !momentKey(v.Key, uploadentity.PrefixMomentVideo) {
|
|
return fmt.Errorf("moment content: video key")
|
|
}
|
|
if v.DurationMS <= 0 || v.DurationMS > MaxVideoMS {
|
|
return fmt.Errorf("moment content: duration")
|
|
}
|
|
if v.CoverKey != "" && !momentKey(v.CoverKey, uploadentity.PrefixMomentImage) {
|
|
return fmt.Errorf("moment content: cover")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func momentKey(key, prefix string) bool {
|
|
key = strings.TrimSpace(key)
|
|
return uploadentity.ValidObjectKey(key) && strings.HasPrefix(key, prefix)
|
|
}
|
|
|
|
func appendKeys(dst []string, keys ...string) []string {
|
|
for _, k := range keys {
|
|
if k != "" {
|
|
dst = append(dst, k)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func ValidCoverKey(key string) bool {
|
|
return momentKey(key, uploadentity.PrefixMomentCover)
|
|
}
|