226 lines
8.3 KiB
Go
226 lines
8.3 KiB
Go
package files
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
// ValidateStickerMaterialUpload is a pure check (no store writes), used by a
|
|
// dry-run preview before AdminUploadStickerMaterial actually materializes the
|
|
// document into a blob + row.
|
|
func (s *Service) ValidateStickerMaterialUpload(fileName string, data []byte) (string, bool) {
|
|
if len(data) == 0 || int64(len(data)) > domain.MaxStickerMaterialDocumentSize {
|
|
return "", false
|
|
}
|
|
return detectStickerMaterialUploadMime(fileName, data)
|
|
}
|
|
|
|
// AdminUploadStickerMaterial turns a raw uploaded file (TGS, plain Lottie
|
|
// JSON, or static WebP) into a loose Document not yet attached to any set,
|
|
// the same shape stickers.createStickerSet/addStickerToSet expect as input.
|
|
// Regular users get this document via messages.uploadMedia before referencing
|
|
// it; the admin console has no such upload step, so this does the equivalent
|
|
// materialization directly from the uploaded bytes.
|
|
func (s *Service) AdminUploadStickerMaterial(ctx context.Context, fileName string, data []byte) (domain.Document, error) {
|
|
if len(data) == 0 || int64(len(data)) > domain.MaxStickerMaterialDocumentSize {
|
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
|
}
|
|
mimeType, ok := detectStickerMaterialUploadMime(fileName, data)
|
|
if !ok {
|
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
|
}
|
|
objectKey, err := s.blobs.Put(ctx, data)
|
|
if err != nil {
|
|
return domain.Document{}, err
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
docID := randomID()
|
|
if err := s.media.PutFileBlob(ctx, domain.FileBlob{
|
|
LocationKey: fmt.Sprintf("doc:%d", docID),
|
|
Backend: domain.MediaBackend(s.blobs.Name()),
|
|
ObjectKey: objectKey,
|
|
Size: int64(len(data)),
|
|
SHA256: append([]byte(nil), sum[:]...),
|
|
MimeType: mimeType,
|
|
}); err != nil {
|
|
return domain.Document{}, err
|
|
}
|
|
doc := domain.Document{
|
|
ID: docID,
|
|
AccessHash: randomID(),
|
|
FileReference: randomFileReference(),
|
|
Date: int(time.Now().Unix()),
|
|
MimeType: mimeType,
|
|
Size: int64(len(data)),
|
|
DCID: s.dc,
|
|
Attributes: []domain.DocumentAttribute{
|
|
{Kind: domain.DocAttrFilename, FileName: strings.TrimSpace(fileName)},
|
|
},
|
|
}
|
|
if err := s.media.PutDocument(ctx, doc); err != nil {
|
|
return domain.Document{}, err
|
|
}
|
|
return doc, nil
|
|
}
|
|
|
|
// detectStickerMaterialUploadMime accepts exactly the raw upload shapes
|
|
// ensureStickerMaterialShape (sticker_creator.go) already knows how to turn
|
|
// into a real sticker document: gzip'd TGS, plain Lottie JSON (gzip'd on
|
|
// attach), or a static WebP image. Anything else (raster PNG, raw video,
|
|
// unrecognized) is rejected rather than guessed at.
|
|
func detectStickerMaterialUploadMime(fileName string, data []byte) (string, bool) {
|
|
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(fileName)))
|
|
webp := isWebPData(data)
|
|
switch {
|
|
case ext == ".tgs" || (len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b):
|
|
return stickerMaterialMimeTGS, validTGSStickerData(data)
|
|
case ext == ".webp" || webp:
|
|
return stickerMaterialMimeWebP, webp
|
|
case ext == ".json":
|
|
_, _, ok := lottieStickerDimensions(normalizeLottieStickerJSON(data))
|
|
return stickerMaterialMimeJSON, ok
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func isWebPData(data []byte) bool {
|
|
return len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "WEBP"
|
|
}
|
|
|
|
// ValidateAdminAddStickerToSet is a pure check (no store writes), used by a
|
|
// dry-run preview before AdminAddStickerToSet actually mutates the pack.
|
|
func (s *Service) ValidateAdminAddStickerToSet(ctx context.Context, setID int64, emoji string) error {
|
|
set, _, found, err := s.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found || set.ID == 0 || set.Deleted || set.Kind == domain.StickerSetKindSystem {
|
|
return domain.ErrStickerSetInvalid
|
|
}
|
|
if len(set.DocumentIDs) >= domain.MaxStickerSetItems {
|
|
return domain.ErrStickerSetTooMuch
|
|
}
|
|
return validateStickerEmoji(strings.TrimSpace(emoji))
|
|
}
|
|
|
|
// ValidateAdminCreateStickerSet is a pure check (no store writes), used by a
|
|
// dry-run preview before AdminCreateStickerSet actually creates the pack.
|
|
func (s *Service) ValidateAdminCreateStickerSet(ctx context.Context, title, shortName, emoji string, kind domain.StickerSetKind) error {
|
|
if err := validateStickerSetTitle(strings.TrimSpace(title)); err != nil {
|
|
return err
|
|
}
|
|
if kind != domain.StickerSetKindEmoji && kind != domain.StickerSetKindMasks && kind != "" {
|
|
return domain.ErrStickerSetTypeInvalid
|
|
}
|
|
normalizedShortName := normalizeStickerSetShortName(shortName)
|
|
if normalizedShortName != "" {
|
|
if err := validateStickerSetShortName(normalizedShortName); err != nil {
|
|
return err
|
|
}
|
|
available, err := s.media.StickerSetShortNameAvailable(ctx, normalizedShortName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !available {
|
|
return domain.ErrStickerSetShortNameOccupied
|
|
}
|
|
}
|
|
return validateStickerEmoji(strings.TrimSpace(emoji))
|
|
}
|
|
|
|
// AdminAddStickerToSet appends an already-materialized document (from
|
|
// AdminUploadStickerMaterial) to an existing pack with no ownership check —
|
|
// same convention as AdminSetStickerSetArchived and friends.
|
|
func (s *Service) AdminAddStickerToSet(ctx context.Context, setID int64, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error) {
|
|
set, docs, found, err := s.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID})
|
|
if err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
if !found || set.ID == 0 || set.Deleted {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
|
}
|
|
if len(set.DocumentIDs) >= domain.MaxStickerSetItems {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetTooMuch
|
|
}
|
|
doc, err := s.loadStickerMaterialDocument(ctx, item.DocumentID, item.DocumentAccessHash)
|
|
if err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
doc, err = s.materialDocumentForStickerSet(ctx, doc, set.ID)
|
|
if err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
if containsInt64(set.DocumentIDs, doc.ID) {
|
|
return set, docs, nil
|
|
}
|
|
emoji := strings.TrimSpace(item.Emoji)
|
|
if err := validateStickerEmoji(emoji); err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
doc, err = s.prepareStickerSetDocument(ctx, doc, set, emoji)
|
|
if err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
set.DocumentIDs = append(set.DocumentIDs, doc.ID)
|
|
set.Count = len(set.DocumentIDs)
|
|
set.Packs = addDocumentToStickerPacks(set.Packs, emoji, doc.ID)
|
|
set.Keywords = upsertStickerKeywords(set.Keywords, parseStickerKeywords(doc.ID, item.Keywords))
|
|
if set.ThumbDocumentID == 0 {
|
|
setStickerSetThumbFromDocument(&set, doc)
|
|
}
|
|
set.Hash = stickerSetHash(set)
|
|
docs = append(docs, doc)
|
|
return s.persistStickerSetMutation(ctx, set, docs, []domain.Document{doc})
|
|
}
|
|
|
|
// AdminRemoveStickerFromSet detaches one document from a pack with no
|
|
// ownership check; see AdminAddStickerToSet for why that's needed here.
|
|
func (s *Service) AdminRemoveStickerFromSet(ctx context.Context, setID int64, documentID int64) (domain.StickerSet, []domain.Document, error) {
|
|
set, docs, found, err := s.ResolveStickerSet(ctx, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID})
|
|
if err != nil {
|
|
return domain.StickerSet{}, nil, err
|
|
}
|
|
if !found || set.ID == 0 || set.Deleted {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
|
|
}
|
|
if len(set.DocumentIDs) <= 1 {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetEmpty
|
|
}
|
|
idx := indexInt64(set.DocumentIDs, documentID)
|
|
if idx < 0 {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
|
}
|
|
var removedDoc domain.Document
|
|
removedFound := false
|
|
for _, d := range docs {
|
|
if d.ID == documentID {
|
|
removedDoc = d
|
|
removedFound = true
|
|
break
|
|
}
|
|
}
|
|
if !removedFound {
|
|
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
|
}
|
|
set.DocumentIDs = removeInt64At(set.DocumentIDs, idx)
|
|
set.Count = len(set.DocumentIDs)
|
|
set.Packs = removeDocumentFromStickerPacks(set.Packs, documentID)
|
|
set.Keywords = removeStickerKeywords(set.Keywords, documentID)
|
|
removedDoc = detachStickerSetFromDocument(removedDoc)
|
|
docs = removeDocumentByID(docs, documentID)
|
|
if set.ThumbDocumentID == documentID {
|
|
clearStickerSetThumb(&set)
|
|
if len(docs) > 0 {
|
|
setStickerSetThumbFromDocument(&set, docs[0])
|
|
}
|
|
}
|
|
set.Hash = stickerSetHash(set)
|
|
return s.persistStickerSetMutation(ctx, set, docs, []domain.Document{removedDoc})
|
|
}
|