owpengram-server/internal/app/files/sticker_management.go
2026-09-01 12:06:31 +03:00

431 lines
14 KiB
Go

package files
import (
"context"
"strings"
"telesrv/internal/domain"
)
func (s *Service) AddStickerToSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error) {
set, docs, err := s.resolveOwnedStickerSet(ctx, actorUserID, ref)
if err != nil {
return domain.StickerSet{}, nil, err
}
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})
}
func (s *Service) RemoveStickerFromSet(ctx context.Context, actorUserID int64, documentID int64, accessHash int64) (domain.StickerSet, []domain.Document, error) {
doc, err := s.loadStickerInputDocument(ctx, documentID, accessHash)
if err != nil {
return domain.StickerSet{}, nil, err
}
setID, setAccessHash, ok := doc.StickerSetRef()
if !ok || setID == 0 {
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
}
set, docs, err := s.resolveOwnedStickerSet(ctx, actorUserID, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash})
if err != nil {
return domain.StickerSet{}, nil, err
}
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
}
set.DocumentIDs = removeInt64At(set.DocumentIDs, idx)
set.Count = len(set.DocumentIDs)
set.Packs = removeDocumentFromStickerPacks(set.Packs, documentID)
set.Keywords = removeStickerKeywords(set.Keywords, documentID)
doc = detachStickerSetFromDocument(doc)
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{doc})
}
func (s *Service) ChangeStickerPosition(ctx context.Context, actorUserID int64, documentID int64, accessHash int64, position int) (domain.StickerSet, []domain.Document, error) {
doc, err := s.loadStickerInputDocument(ctx, documentID, accessHash)
if err != nil {
return domain.StickerSet{}, nil, err
}
setID, setAccessHash, ok := doc.StickerSetRef()
if !ok || setID == 0 {
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
}
set, docs, err := s.resolveOwnedStickerSet(ctx, actorUserID, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash})
if err != nil {
return domain.StickerSet{}, nil, err
}
if position < 0 || position >= len(set.DocumentIDs) {
return domain.StickerSet{}, nil, domain.ErrStickerSetPositionInvalid
}
from := indexInt64(set.DocumentIDs, documentID)
if from < 0 {
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
}
set.DocumentIDs = moveInt64(set.DocumentIDs, from, position)
docs = orderDocuments(docs, set.DocumentIDs)
set.Hash = stickerSetHash(set)
return s.persistStickerSetMutation(ctx, set, docs, nil)
}
func (s *Service) RenameStickerSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef, title string) (domain.StickerSet, []domain.Document, error) {
set, docs, err := s.resolveOwnedStickerSet(ctx, actorUserID, ref)
if err != nil {
return domain.StickerSet{}, nil, err
}
title = strings.TrimSpace(title)
if err := validateStickerSetTitle(title); err != nil {
return domain.StickerSet{}, nil, err
}
set.Title = title
set.Hash = stickerSetHash(set)
return s.persistStickerSetMutation(ctx, set, docs, nil)
}
func (s *Service) DeleteStickerSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef) (domain.StickerSetKind, error) {
set, _, err := s.resolveOwnedStickerSet(ctx, actorUserID, ref)
if err != nil {
return "", err
}
if err := s.media.DeleteStickerSet(ctx, set.ID, actorUserID); err != nil {
return "", err
}
s.deleteCachedStickerSet(set)
return set.Kind, nil
}
// AdminSetStickerSetArchived toggles a set's archived flag with no ownership
// check, so admins can hide/show any pack — including seed-imported system
// and regular packs, which have no creator_user_id to match against.
func (s *Service) AdminSetStickerSetArchived(ctx context.Context, setID int64, archived bool) (bool, error) {
set, found, err := s.media.GetStickerSetByID(ctx, setID)
if err != nil {
return false, err
}
if !found || set.Deleted {
return false, domain.ErrStickerSetInvalid
}
if set.Archived == archived {
return false, nil
}
set.Archived = archived
if err := s.media.UpdateStickerSet(ctx, set, nil); err != nil {
return false, err
}
s.deleteCachedStickerSet(set)
return true, nil
}
// AdminSetStickerSetSortOrder sets a set's display sort order with no
// ownership check; see AdminSetStickerSetArchived for why that's needed here.
func (s *Service) AdminSetStickerSetSortOrder(ctx context.Context, setID int64, order int) (bool, error) {
set, found, err := s.media.GetStickerSetByID(ctx, setID)
if err != nil {
return false, err
}
if !found || set.Deleted {
return false, domain.ErrStickerSetInvalid
}
if set.SortOrder == order {
return false, nil
}
set.SortOrder = order
if err := s.media.UpdateStickerSet(ctx, set, nil); err != nil {
return false, err
}
s.deleteCachedStickerSet(set)
return true, nil
}
// AdminRenameStickerSet renames a set with no ownership check; see
// AdminSetStickerSetArchived for why that's needed here.
func (s *Service) AdminRenameStickerSet(ctx context.Context, setID int64, title string) (domain.StickerSet, error) {
title = strings.TrimSpace(title)
if err := validateStickerSetTitle(title); err != nil {
return domain.StickerSet{}, err
}
set, found, err := s.media.GetStickerSetByID(ctx, setID)
if err != nil {
return domain.StickerSet{}, err
}
if !found || set.Deleted {
return domain.StickerSet{}, domain.ErrStickerSetInvalid
}
set.Title = title
set.Hash = stickerSetHash(set)
if err := s.media.UpdateStickerSet(ctx, set, nil); err != nil {
return domain.StickerSet{}, err
}
s.deleteCachedStickerSet(set)
return set, nil
}
// AdminDeleteStickerSet deletes (soft-delete) a set with no ownership check;
// see AdminSetStickerSetArchived for why that's needed here. Safe to bypass
// ownership for: sticker_sets has no incoming foreign keys, so there's no
// cascade to worry about. Seed-imported sets will reappear on next restart
// if their source files are still under data/sticker-seed — this only
// removes the DB row.
func (s *Service) AdminDeleteStickerSet(ctx context.Context, setID int64) (domain.StickerSetKind, error) {
set, found, err := s.media.GetStickerSetByID(ctx, setID)
if err != nil {
return "", err
}
if !found || set.Deleted {
return "", domain.ErrStickerSetInvalid
}
if err := s.media.AdminDeleteStickerSet(ctx, setID); err != nil {
return "", err
}
s.deleteCachedStickerSet(set)
return set.Kind, nil
}
func (s *Service) resolveOwnedStickerSet(ctx context.Context, actorUserID int64, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, error) {
if actorUserID <= 0 {
return domain.StickerSet{}, nil, domain.ErrStickerSetCreatorInvalid
}
if ref.Kind != domain.StickerSetRefByID && ref.Kind != domain.StickerSetRefByShortName {
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
}
set, docs, found, err := s.ResolveStickerSet(ctx, ref)
if err != nil {
return domain.StickerSet{}, nil, err
}
if !found || set.ID == 0 || set.Deleted {
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
}
if ref.Kind == domain.StickerSetRefByID && set.AccessHash != ref.AccessHash {
return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid
}
if set.CreatorUserID != actorUserID {
return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned
}
return set, docs, nil
}
func (s *Service) loadStickerInputDocument(ctx context.Context, documentID int64, accessHash int64) (domain.Document, error) {
if documentID == 0 || accessHash == 0 {
return domain.Document{}, domain.ErrStickerSetFileInvalid
}
docs, err := s.media.GetDocuments(ctx, []int64{documentID})
if err != nil {
return domain.Document{}, err
}
if len(docs) != 1 || docs[0].ID != documentID || docs[0].AccessHash != accessHash || !docs[0].IsStickerLike() {
return domain.Document{}, domain.ErrStickerSetFileInvalid
}
return docs[0], nil
}
func (s *Service) loadStickerMaterialDocument(ctx context.Context, documentID int64, accessHash int64) (domain.Document, error) {
if documentID == 0 || accessHash == 0 {
return domain.Document{}, domain.ErrStickerSetFileInvalid
}
docs, err := s.media.GetDocuments(ctx, []int64{documentID})
if err != nil {
return domain.Document{}, err
}
if len(docs) != 1 || docs[0].ID != documentID || docs[0].AccessHash != accessHash || !docs[0].IsStickerSetMaterial() {
return domain.Document{}, domain.ErrStickerSetFileInvalid
}
return docs[0], nil
}
func (s *Service) persistStickerSetMutation(ctx context.Context, set domain.StickerSet, docs []domain.Document, changedDocs []domain.Document) (domain.StickerSet, []domain.Document, error) {
if err := s.media.UpdateStickerSet(ctx, set, changedDocs); err != nil {
return domain.StickerSet{}, nil, err
}
ordered := orderDocuments(docs, set.DocumentIDs)
s.cacheStickerSet(set, ordered)
return set, ordered, nil
}
func (s *Service) deleteCachedStickerSet(set domain.StickerSet) {
if s.stickerSetNegCache != nil {
s.stickerSetNegCache.put(domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: set.ID})
if set.ShortName != "" {
s.stickerSetNegCache.put(domain.StickerSetRef{Kind: domain.StickerSetRefByShortName, ShortName: set.ShortName})
}
}
if s.stickerSetCache != nil {
s.stickerSetCache.delete(set)
}
}
func addDocumentToStickerPacks(packs []domain.StickerPack, emoji string, documentID int64) []domain.StickerPack {
out := copyStickerPacks(packs)
for i := range out {
if out[i].Emoticon == emoji {
if !containsInt64(out[i].DocumentIDs, documentID) {
out[i].DocumentIDs = append(out[i].DocumentIDs, documentID)
}
return out
}
}
return append(out, domain.StickerPack{Emoticon: emoji, DocumentIDs: []int64{documentID}})
}
func removeDocumentFromStickerPacks(packs []domain.StickerPack, documentID int64) []domain.StickerPack {
out := make([]domain.StickerPack, 0, len(packs))
for _, pack := range packs {
ids := removeInt64Value(pack.DocumentIDs, documentID)
if len(ids) == 0 {
continue
}
out = append(out, domain.StickerPack{Emoticon: pack.Emoticon, DocumentIDs: ids})
}
return out
}
func upsertStickerKeywords(in []domain.StickerKeyword, kw domain.StickerKeyword) []domain.StickerKeyword {
out := removeStickerKeywords(in, kw.DocumentID)
if len(kw.Keywords) == 0 {
return out
}
return append(out, kw)
}
func removeStickerKeywords(in []domain.StickerKeyword, documentID int64) []domain.StickerKeyword {
out := make([]domain.StickerKeyword, 0, len(in))
for _, kw := range in {
if kw.DocumentID == documentID {
continue
}
out = append(out, domain.StickerKeyword{DocumentID: kw.DocumentID, Keywords: append([]string(nil), kw.Keywords...)})
}
return out
}
func detachStickerSetFromDocument(doc domain.Document) domain.Document {
attrs := append([]domain.DocumentAttribute(nil), doc.Attributes...)
for i := range attrs {
if attrs[i].Kind != domain.DocAttrSticker && attrs[i].Kind != domain.DocAttrCustomEmoji {
continue
}
attrs[i].StickerSetID = 0
attrs[i].StickerSetAccessHash = 0
attrs[i].Mask = false
attrs[i].TextColor = false
break
}
doc.Attributes = attrs
return doc
}
func setStickerSetThumbFromDocument(set *domain.StickerSet, doc domain.Document) {
set.ThumbDocumentID = doc.ID
set.Thumbs = copyPhotoSizes(doc.Thumbs)
set.ThumbDCID = doc.DCID
set.ThumbVersion = 0
if len(set.Thumbs) > 0 {
set.ThumbVersion = 1
}
}
func clearStickerSetThumb(set *domain.StickerSet) {
set.ThumbDocumentID = 0
set.Thumbs = nil
set.ThumbDCID = 0
set.ThumbVersion = 0
}
func copyStickerPacks(packs []domain.StickerPack) []domain.StickerPack {
out := append([]domain.StickerPack(nil), packs...)
for i := range out {
out[i].DocumentIDs = append([]int64(nil), out[i].DocumentIDs...)
}
return out
}
func containsInt64(in []int64, value int64) bool {
return indexInt64(in, value) >= 0
}
func indexInt64(in []int64, value int64) int {
for i, v := range in {
if v == value {
return i
}
}
return -1
}
func removeInt64At(in []int64, idx int) []int64 {
out := append([]int64(nil), in[:idx]...)
return append(out, in[idx+1:]...)
}
func removeInt64Value(in []int64, value int64) []int64 {
out := make([]int64, 0, len(in))
for _, v := range in {
if v != value {
out = append(out, v)
}
}
return out
}
func moveInt64(in []int64, from, to int) []int64 {
out := append([]int64(nil), in...)
if from == to {
return out
}
value := out[from]
out = append(out[:from], out[from+1:]...)
if to >= len(out) {
return append(out, value)
}
out = append(out[:to], append([]int64{value}, out[to:]...)...)
return out
}
func removeDocumentByID(docs []domain.Document, documentID int64) []domain.Document {
out := make([]domain.Document, 0, len(docs))
for _, doc := range docs {
if doc.ID != documentID {
out = append(out, doc)
}
}
return out
}