Ctrl K
ComunidadGuías…

internal/domain/media_category.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package domain

// MediaCategory is the base category of a shared-media tab. A message can belong to several categories at once
// (e.g. an image with a link = Photo + URL). It is stored in the media index table's category column (SMALLINT).
//
// Only "base categories" are stored here; the client's composite filters (PhotoVideo / RoundVoice) are mapped at query time to
// the union of several base categories (see the callers of MediaCategoriesForFilter).
type MediaCategory int16

const (
	MediaCategoryNone       MediaCategory = 0
	MediaCategoryPhoto      MediaCategory = 1 // media.kind=photo
	MediaCategoryVideo      MediaCategory = 2 // document has the video attribute and is not round
	MediaCategoryGif        MediaCategory = 3 // document has the animated attribute
	MediaCategoryFile       MediaCategory = 4 // generic document (no video/audio/animated/sticker attributes)
	MediaCategoryMusic      MediaCategory = 5 // document has the audio attribute and voice=false
	MediaCategoryVoice      MediaCategory = 6 // document has the audio attribute and voice=true
	MediaCategoryRoundVideo MediaCategory = 7 // document has the video attribute and round_message=true (video message)
	MediaCategoryURL        MediaCategory = 8 // text has a url/text_url/email entity or messageMediaWebPage
	MediaCategoryPoll       MediaCategory = 9 // media.kind=poll
	// MediaCategoryGeoLive is the internal index category for messages.getRecentLocations.
	// It does not map to an ordinary shared-media filter; it is only used to read live-location messages in a bounded way per peer.
	MediaCategoryGeoLive MediaCategory = 10 // media.kind=geo_live
)

// MediaCategoryCounts is the exact counts aggregated by base category from the shared-media index.
type MediaCategoryCounts map[MediaCategory]int

// CountAny returns the count for the union of the given base categories. Current composite filters consist only of disjoint categories
// (PhotoVideo=Photo+Video, RoundVoice=Voice+RoundVideo), so they can be summed directly.
func (c MediaCategoryCounts) CountAny(categories []MediaCategory) int {
	if len(categories) == 0 {
		return 0
	}
	seen := make(map[MediaCategory]struct{}, len(categories))
	total := 0
	for _, category := range categories {
		if category == MediaCategoryNone {
			continue
		}
		if _, ok := seen[category]; ok {
			continue
		}
		seen[category] = struct{}{}
		total += c[category]
	}
	return total
}

// MediaSearchRequest is the input of a shared-media tab paginated query (the media-filter branch of messages.search).
// Categories is the union of base categories the tab maps to (PhotoVideo→[Photo,Video], RoundVoice→[Voice,RoundVideo]).
// OffsetID locates the first strictly older message; a negative AddOffset fetches toward newer items (possibly including the cursor itself).
// MaxID/MinID and MaxDate/MinDate are all open intervals; counts ignore the pagination offset.
type MediaSearchRequest struct {
	Categories     []MediaCategory
	Query          string
	SenderUserID   int64
	MinDate        int
	MaxDate        int
	TopMsgID       int
	SavedPeer      Peer
	SavedReactions []MessageReaction
	OffsetID       int
	AddOffset      int
	Limit          int
	MaxID          int
	MinID          int
	KnownCount     int
	HasKnownCount  bool
}

// ClassifyMediaCategories returns every shared-media category a message belongs to (possibly empty: no media and no link,
// or payloads like stickers/geo/contacts that belong to no media tab). It is the single source of truth for media classification: the write path uses it
// to maintain the index, and migration backfill must match its semantics.
func ClassifyMediaCategories(media *MessageMedia, entities []MessageEntity) []MediaCategory {
	cats := make([]MediaCategory, 0, 2)
	add := func(category MediaCategory) {
		if category == MediaCategoryNone {
			return
		}
		for _, existing := range cats {
			if existing == category {
				return
			}
		}
		cats = append(cats, category)
	}
	if media != nil {
		switch media.Kind {
		case MessageMediaKindPhoto:
			add(MediaCategoryPhoto)
		case MessageMediaKindDocument:
			if c, ok := classifyDocumentCategory(media.Document); ok {
				add(c)
			}
		case MessageMediaKindPoll:
			add(MediaCategoryPoll)
		case MessageMediaKindGeoLive:
			add(MediaCategoryGeoLive)
		case MessageMediaKindWebPage:
			add(MediaCategoryURL)
		}
	}
	if hasURLEntity(entities) {
		add(MediaCategoryURL)
	}
	return cats
}

// classifyDocumentCategory decides which media category a document falls into based on TL DocumentAttribute.
// The client's tab UI decides using TL attributes together with the GIFv MIME. Priority: sticker (belongs to no tab) >
// canonical animated MP4 (GIF) > audio (music/voice) > video (video/round) > generic file.
func classifyDocumentCategory(doc *Document) (MediaCategory, bool) {
	if doc == nil {
		return MediaCategoryNone, false
	}
	var (
		hasSticker  bool
		hasAnimated bool
		hasAudio    bool
		audioVoice  bool
		hasVideo    bool
		videoRound  bool
	)
	for _, a := range doc.Attributes {
		switch a.Kind {
		case DocAttrSticker, DocAttrCustomEmoji:
			hasSticker = true
		case DocAttrAnimated:
			hasAnimated = true
		case DocAttrAudio:
			hasAudio = true
			if a.Voice {
				audioVoice = true
			}
		case DocAttrVideo:
			hasVideo = true
			if a.RoundMessage {
				videoRound = true
			}
		}
	}
	switch {
	case hasSticker:
		return MediaCategoryNone, false // stickers/custom emoji do not appear in shared media
	case hasAnimated && doc.IsGif():
		return MediaCategoryGif, true
	case hasAudio:
		if audioVoice {
			return MediaCategoryVoice, true
		}
		return MediaCategoryMusic, true
	case hasVideo:
		if videoRound {
			return MediaCategoryRoundVideo, true
		}
		return MediaCategoryVideo, true
	default:
		return MediaCategoryFile, true
	}
}

func hasURLEntity(entities []MessageEntity) bool {
	for _, e := range entities {
		if e.Type == MessageEntityURL || e.Type == MessageEntityTextURL || e.Type == MessageEntityEmail {
			return true
		}
	}
	return false
}