package entity import ( "bytes" "encoding/json" "fmt" "strings" uploadentity "spark-server/internal/module/upload/entity" ) // Content is persisted JSON for images/video. type Content struct { Images []*Image `json:"images,omitempty"` Video *Video `json:"video,omitempty"` } // Image matches chat/moment image JSON field names. 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/moment 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("square content: %w", err) } b := bytes.TrimSuffix(buf.Bytes(), []byte{'\n'}) if len(b) > MaxContentBytes { return nil, fmt.Errorf("square 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("square 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("square content: image and video") } if len(c.Images) > MaxImages { return fmt.Errorf("square 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 || !squareKey(img.Key, uploadentity.PrefixSquareImage) { return fmt.Errorf("square content: image key") } if img.ThumbKey != "" && !squareKey(img.ThumbKey, uploadentity.PrefixSquareImage) { return fmt.Errorf("square content: thumb") } if img.OrigKey != "" && !squareKey(img.OrigKey, uploadentity.PrefixSquareImage) { return fmt.Errorf("square content: orig") } return nil } func (v *Video) validate() error { if v == nil || !squareKey(v.Key, uploadentity.PrefixSquareVideo) { return fmt.Errorf("square content: video key") } if v.DurationMS <= 0 || v.DurationMS > MaxVideoMS { return fmt.Errorf("square content: duration") } if v.CoverKey != "" && !squareKey(v.CoverKey, uploadentity.PrefixSquareImage) { return fmt.Errorf("square content: cover") } return nil } func squareKey(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 }