fixed stickers and emojies creation packs
This commit is contained in:
parent
43894f2fc9
commit
139967399d
16 changed files with 771 additions and 22 deletions
|
|
@ -41,6 +41,9 @@ const (
|
|||
ActionSetStickerSetSortOrder = "stickers.set_sort_order"
|
||||
ActionRenameStickerSet = "stickers.rename"
|
||||
ActionDeleteStickerSet = "stickers.delete"
|
||||
ActionCreateStickerSet = "stickers.create"
|
||||
ActionAddStickerToSet = "stickers.add_sticker"
|
||||
ActionRemoveStickerFromSet = "stickers.remove_sticker"
|
||||
|
||||
maxCommandIDLength = 128
|
||||
maxActorLength = 128
|
||||
|
|
@ -139,6 +142,13 @@ type StickerSetsService interface {
|
|||
AdminSetStickerSetSortOrder(ctx context.Context, setID int64, order int) (bool, error)
|
||||
AdminRenameStickerSet(ctx context.Context, setID int64, title string) (domain.StickerSet, error)
|
||||
AdminDeleteStickerSet(ctx context.Context, setID int64) (domain.StickerSetKind, error)
|
||||
// ValidateStickerMaterialUpload is a pure check (no store writes) so a dry-run
|
||||
// preview can validate an uploaded file's shape without materializing it.
|
||||
ValidateStickerMaterialUpload(fileName string, data []byte) (mimeType string, ok bool)
|
||||
AdminUploadStickerMaterial(ctx context.Context, fileName string, data []byte) (domain.Document, error)
|
||||
AdminCreateStickerSet(ctx context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error)
|
||||
AdminAddStickerToSet(ctx context.Context, setID int64, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error)
|
||||
AdminRemoveStickerFromSet(ctx context.Context, setID int64, documentID int64) (domain.StickerSet, []domain.Document, error)
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
|
|
@ -324,6 +334,32 @@ type DeleteStickerSetRequest struct {
|
|||
SetID int64 `json:"set_id"`
|
||||
}
|
||||
|
||||
type CreateStickerSetRequest struct {
|
||||
CommandMeta
|
||||
Title string `json:"title"`
|
||||
ShortName string `json:"short_name"`
|
||||
Kind string `json:"kind"`
|
||||
Emoji string `json:"emoji"`
|
||||
Keywords string `json:"keywords,omitempty"`
|
||||
FileName string `json:"file_name"`
|
||||
Data []byte `json:"-"`
|
||||
}
|
||||
|
||||
type AddStickerToSetRequest struct {
|
||||
CommandMeta
|
||||
SetID int64 `json:"set_id"`
|
||||
Emoji string `json:"emoji"`
|
||||
Keywords string `json:"keywords,omitempty"`
|
||||
FileName string `json:"file_name"`
|
||||
Data []byte `json:"-"`
|
||||
}
|
||||
|
||||
type RemoveStickerFromSetRequest struct {
|
||||
CommandMeta
|
||||
SetID int64 `json:"set_id"`
|
||||
DocumentID int64 `json:"document_id"`
|
||||
}
|
||||
|
||||
type StarGiftCollectibleAnimationUpload struct {
|
||||
Name string `json:"name"`
|
||||
RarityPermille int `json:"rarity_permille"`
|
||||
|
|
@ -1505,6 +1541,109 @@ func (s *Service) DeleteStickerSet(ctx context.Context, req DeleteStickerSetRequ
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) CreateStickerSet(ctx context.Context, req CreateStickerSetRequest) (CommandResult, error) {
|
||||
if s == nil || s.stickerSets == nil {
|
||||
return CommandResult{}, fmt.Errorf("sticker sets service is not configured")
|
||||
}
|
||||
if strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.ShortName) == "" || strings.TrimSpace(req.Emoji) == "" {
|
||||
return CommandResult{}, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
mimeType, ok := s.stickerSets.ValidateStickerMaterialUpload(req.FileName, req.Data)
|
||||
if !ok {
|
||||
return CommandResult{}, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
kind := domain.StickerSetKindStickers
|
||||
if req.Kind == string(domain.StickerSetKindEmoji) {
|
||||
kind = domain.StickerSetKindEmoji
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionCreateStickerSet, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{
|
||||
"title": req.Title, "short_name": req.ShortName, "kind": string(kind),
|
||||
"file_name": req.FileName, "mime_type": mimeType, "bytes": len(req.Data),
|
||||
}
|
||||
if req.DryRun {
|
||||
return CommandResult{Message: "sticker pack validated", Details: details}, nil
|
||||
}
|
||||
doc, err := s.stickerSets.AdminUploadStickerMaterial(ctx, req.FileName, req.Data)
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
set, _, err := s.stickerSets.AdminCreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||
Title: req.Title,
|
||||
ShortName: req.ShortName,
|
||||
Kind: kind,
|
||||
Items: []domain.StickerSetItemInput{{
|
||||
DocumentID: doc.ID,
|
||||
DocumentAccessHash: doc.AccessHash,
|
||||
Emoji: req.Emoji,
|
||||
Keywords: req.Keywords,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
details["set_id"] = strconv.FormatInt(set.ID, 10)
|
||||
details["short_name"] = set.ShortName
|
||||
return CommandResult{Message: "sticker pack created", Details: details}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) AddStickerToSet(ctx context.Context, req AddStickerToSetRequest) (CommandResult, error) {
|
||||
if s == nil || s.stickerSets == nil || req.SetID <= 0 {
|
||||
return CommandResult{}, fmt.Errorf("valid sticker set and service are required")
|
||||
}
|
||||
if strings.TrimSpace(req.Emoji) == "" {
|
||||
return CommandResult{}, domain.ErrStickerSetEmojiInvalid
|
||||
}
|
||||
mimeType, ok := s.stickerSets.ValidateStickerMaterialUpload(req.FileName, req.Data)
|
||||
if !ok {
|
||||
return CommandResult{}, domain.ErrStickerSetFileInvalid
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionAddStickerToSet, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{
|
||||
"set_id": strconv.FormatInt(req.SetID, 10), "emoji": req.Emoji,
|
||||
"file_name": req.FileName, "mime_type": mimeType, "bytes": len(req.Data),
|
||||
}
|
||||
if req.DryRun {
|
||||
return CommandResult{Message: "sticker upload validated", Details: details}, nil
|
||||
}
|
||||
doc, err := s.stickerSets.AdminUploadStickerMaterial(ctx, req.FileName, req.Data)
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
set, _, err := s.stickerSets.AdminAddStickerToSet(ctx, req.SetID, domain.StickerSetItemInput{
|
||||
DocumentID: doc.ID,
|
||||
DocumentAccessHash: doc.AccessHash,
|
||||
Emoji: req.Emoji,
|
||||
Keywords: req.Keywords,
|
||||
})
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
details["document_id"] = strconv.FormatInt(doc.ID, 10)
|
||||
details["count"] = set.Count
|
||||
return CommandResult{Message: "sticker added", Details: details}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) RemoveStickerFromSet(ctx context.Context, req RemoveStickerFromSetRequest) (CommandResult, error) {
|
||||
if s == nil || s.stickerSets == nil || req.SetID <= 0 || req.DocumentID <= 0 {
|
||||
return CommandResult{}, fmt.Errorf("valid sticker set, document and service are required")
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionRemoveStickerFromSet, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{"set_id": strconv.FormatInt(req.SetID, 10), "document_id": strconv.FormatInt(req.DocumentID, 10)}
|
||||
if req.DryRun {
|
||||
return CommandResult{Message: "sticker removal validated", Details: details}, nil
|
||||
}
|
||||
set, _, err := s.stickerSets.AdminRemoveStickerFromSet(ctx, req.SetID, req.DocumentID)
|
||||
if err != nil {
|
||||
return CommandResult{Details: details}, err
|
||||
}
|
||||
details["count"] = set.Count
|
||||
return CommandResult{Message: "sticker removed", Details: details}, nil
|
||||
})
|
||||
}
|
||||
|
||||
const maxStickerDocumentBytes = 8 << 20
|
||||
|
||||
// StickerDocumentAnimation returns a sticker/custom-emoji document's preview
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ type Service interface {
|
|||
SetStickerSetSortOrder(ctx context.Context, req admin.SetStickerSetSortOrderRequest) (admin.CommandResult, error)
|
||||
RenameStickerSet(ctx context.Context, req admin.RenameStickerSetRequest) (admin.CommandResult, error)
|
||||
DeleteStickerSet(ctx context.Context, req admin.DeleteStickerSetRequest) (admin.CommandResult, error)
|
||||
CreateStickerSet(ctx context.Context, req admin.CreateStickerSetRequest) (admin.CommandResult, error)
|
||||
AddStickerToSet(ctx context.Context, req admin.AddStickerToSetRequest) (admin.CommandResult, error)
|
||||
RemoveStickerFromSet(ctx context.Context, req admin.RemoveStickerFromSetRequest) (admin.CommandResult, error)
|
||||
StickerDocumentAnimation(ctx context.Context, documentID int64) ([]byte, string, bool, error)
|
||||
StarGiftAnimation(ctx context.Context, giftID int64) ([]byte, bool, error)
|
||||
StarGiftCollectibles(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error)
|
||||
|
|
@ -119,6 +122,9 @@ func (s *Server) routes() http.Handler {
|
|||
mux.HandleFunc("POST /v1/stickers/set-sort-order", s.authenticated(s.handleSetStickerSetSortOrder))
|
||||
mux.HandleFunc("POST /v1/stickers/rename", s.authenticated(s.handleRenameStickerSet))
|
||||
mux.HandleFunc("POST /v1/stickers/delete", s.authenticated(s.handleDeleteStickerSet))
|
||||
mux.HandleFunc("POST /v1/stickers/create", s.authenticated(s.handleCreateStickerSet))
|
||||
mux.HandleFunc("POST /v1/stickers/add", s.authenticated(s.handleAddStickerToSet))
|
||||
mux.HandleFunc("POST /v1/stickers/remove", s.authenticated(s.handleRemoveStickerFromSet))
|
||||
mux.HandleFunc("GET /v1/stickers/documents/{id}/animation", s.authenticated(s.handleStickerDocumentAnimation))
|
||||
mux.HandleFunc("GET /v1/gifts/{id}/animation", s.authenticated(s.handleStarGiftAnimation))
|
||||
mux.HandleFunc("GET /v1/gifts/{id}/collectibles", s.authenticated(s.handleStarGiftCollectibles))
|
||||
|
|
@ -448,6 +454,83 @@ func (s *Server) handleDeleteStickerSet(w http.ResponseWriter, r *http.Request)
|
|||
writeCommandResult(w, result, err)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateStickerSet(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
r.Body = http.MaxBytesReader(w, r.Body, domain.MaxStickerMaterialDocumentSize+(1<<20))
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
||||
return
|
||||
}
|
||||
if r.MultipartForm != nil {
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
}
|
||||
var req admin.CreateStickerSetRequest
|
||||
dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata")))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error())
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "sticker file is required")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, domain.MaxStickerMaterialDocumentSize+1))
|
||||
if err != nil || len(data) == 0 || int64(len(data)) > domain.MaxStickerMaterialDocumentSize {
|
||||
writeError(w, http.StatusBadRequest, "sticker file is empty or too large")
|
||||
return
|
||||
}
|
||||
req.FileName = header.Filename
|
||||
req.Data = data
|
||||
result, err := s.svc.CreateStickerSet(r.Context(), req)
|
||||
writeCommandResult(w, result, err)
|
||||
}
|
||||
|
||||
func (s *Server) handleAddStickerToSet(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
r.Body = http.MaxBytesReader(w, r.Body, domain.MaxStickerMaterialDocumentSize+(1<<20))
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
||||
return
|
||||
}
|
||||
if r.MultipartForm != nil {
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
}
|
||||
var req admin.AddStickerToSetRequest
|
||||
dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata")))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error())
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "sticker file is required")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, domain.MaxStickerMaterialDocumentSize+1))
|
||||
if err != nil || len(data) == 0 || int64(len(data)) > domain.MaxStickerMaterialDocumentSize {
|
||||
writeError(w, http.StatusBadRequest, "sticker file is empty or too large")
|
||||
return
|
||||
}
|
||||
req.FileName = header.Filename
|
||||
req.Data = data
|
||||
result, err := s.svc.AddStickerToSet(r.Context(), req)
|
||||
writeCommandResult(w, result, err)
|
||||
}
|
||||
|
||||
func (s *Server) handleRemoveStickerFromSet(w http.ResponseWriter, r *http.Request) {
|
||||
var req admin.RemoveStickerFromSetRequest
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
result, err := s.svc.RemoveStickerFromSet(r.Context(), req)
|
||||
writeCommandResult(w, result, err)
|
||||
}
|
||||
|
||||
func (s *Server) handleStickerDocumentAnimation(w http.ResponseWriter, r *http.Request) {
|
||||
documentID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || documentID <= 0 {
|
||||
|
|
|
|||
|
|
@ -291,6 +291,18 @@ func (fakeService) DeleteStickerSet(_ context.Context, req admin.DeleteStickerSe
|
|||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||
}
|
||||
|
||||
func (fakeService) CreateStickerSet(_ context.Context, req admin.CreateStickerSetRequest) (admin.CommandResult, error) {
|
||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||
}
|
||||
|
||||
func (fakeService) AddStickerToSet(_ context.Context, req admin.AddStickerToSetRequest) (admin.CommandResult, error) {
|
||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||
}
|
||||
|
||||
func (fakeService) RemoveStickerFromSet(_ context.Context, req admin.RemoveStickerFromSetRequest) (admin.CommandResult, error) {
|
||||
return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil
|
||||
}
|
||||
|
||||
func (fakeService) StickerDocumentAnimation(context.Context, int64) ([]byte, string, bool, error) {
|
||||
return nil, "", false, nil
|
||||
}
|
||||
|
|
|
|||
185
internal/app/files/sticker_admin.go
Normal file
185
internal/app/files/sticker_admin.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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"
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
|
@ -62,6 +62,23 @@ func (s *Service) CreateStickerSet(ctx context.Context, req domain.CreateSticker
|
|||
if req.CreatorUserID <= 0 {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetCreatorInvalid
|
||||
}
|
||||
return s.createStickerSet(ctx, req)
|
||||
}
|
||||
|
||||
// AdminCreateStickerSet creates a pack with no owning user (CreatorUserID
|
||||
// forced to 0) so it's editable only through the Admin* bypass methods —
|
||||
// the same convention seed-imported packs already use. Unlike CreateStickerSet,
|
||||
// the caller must supply a non-empty ShortName: short-name suggestion needs a
|
||||
// real user id to build a stable suffix, which an unowned pack doesn't have.
|
||||
func (s *Service) AdminCreateStickerSet(ctx context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error) {
|
||||
req.CreatorUserID = 0
|
||||
if strings.TrimSpace(req.ShortName) == "" {
|
||||
return domain.StickerSet{}, nil, domain.ErrStickerSetShortNameInvalid
|
||||
}
|
||||
return s.createStickerSet(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) createStickerSet(ctx context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error) {
|
||||
title := strings.TrimSpace(req.Title)
|
||||
if err := validateStickerSetTitle(title); err != nil {
|
||||
return domain.StickerSet{}, nil, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue