190 lines
6.4 KiB
Go
190 lines
6.4 KiB
Go
package domain
|
||
|
||
// MediaCategory 是共享媒体标签页的基础类别。一条消息可同时属于多个类别
|
||
// (例如带链接的图片 = Photo + URL)。落库为媒体索引表的 category 列(SMALLINT)。
|
||
//
|
||
// 这里只存"基础类别";客户端的复合过滤器(PhotoVideo / RoundVoice)在查询期映射为
|
||
// 多个基础类别的并集(见 MediaCategoriesForFilter 的调用方)。
|
||
type MediaCategory int16
|
||
|
||
const (
|
||
MediaCategoryNone MediaCategory = 0
|
||
MediaCategoryPhoto MediaCategory = 1 // media.kind=photo
|
||
MediaCategoryVideo MediaCategory = 2 // document 含 video 属性且非 round
|
||
MediaCategoryGif MediaCategory = 3 // document 含 animated 属性
|
||
MediaCategoryFile MediaCategory = 4 // 通用文档(无 video/audio/animated/sticker 属性)
|
||
MediaCategoryMusic MediaCategory = 5 // document 含 audio 属性且 voice=false
|
||
MediaCategoryVoice MediaCategory = 6 // document 含 audio 属性且 voice=true
|
||
MediaCategoryRoundVideo MediaCategory = 7 // document 含 video 属性且 round_message=true(视频消息)
|
||
MediaCategoryURL MediaCategory = 8 // 文本含 url/text_url/email 实体或 messageMediaWebPage
|
||
MediaCategoryPoll MediaCategory = 9 // media.kind=poll
|
||
// MediaCategoryGeoLive 是 messages.getRecentLocations 的内部索引类别。
|
||
// 它不映射到普通共享媒体筛选器,只用于按 peer 有界读取 live-location 消息。
|
||
MediaCategoryGeoLive MediaCategory = 10 // media.kind=geo_live
|
||
)
|
||
|
||
// MediaCategoryCounts 是共享媒体索引按基础类别聚合出的精确计数。
|
||
type MediaCategoryCounts map[MediaCategory]int
|
||
|
||
// CountAny 返回给定基础类别并集的计数。当前复合 filter 只由互斥类别组成
|
||
// (PhotoVideo=Photo+Video, RoundVoice=Voice+RoundVideo),因此可直接相加。
|
||
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 是共享媒体标签页分页查询的入参(messages.search 媒体过滤分支)。
|
||
// Categories 是该标签页映射到的基础类别并集(PhotoVideo→[Photo,Video]、RoundVoice→[Voice,RoundVideo])。
|
||
// OffsetID 定位第一条严格更旧的消息;负 AddOffset 向更新侧取数(可含游标本身)。
|
||
// MaxID/MinID、MaxDate/MinDate 均为开区间;计数忽略分页偏移。
|
||
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 返回一条消息所属的全部共享媒体类别(可为空:无媒体且无链接,
|
||
// 或贴纸/地理/联系人等不进任何媒体标签页的载荷)。是媒体索引的唯一分类真值,写路径据此
|
||
// 维护索引、迁移回填须与其语义一致。
|
||
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 按 TL DocumentAttribute 判定一个文档落入哪个媒体类别。
|
||
// 客户端的标签页 UI 以 TL 属性和 GIFv MIME 共同判定。优先级:sticker(不入任何标签页)>
|
||
// canonical animated MP4 (GIF) > audio(music/voice) > video(video/round) > 通用文件。
|
||
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 // 贴纸/自定义 emoji 不出现在共享媒体
|
||
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
|
||
}
|
||
}
|
||
|
||
// DocumentMediaCategory returns the single document-attribute category
|
||
// (Video/Gif/Music/Voice/RoundVideo/File) for a document media payload, used
|
||
// to populate the documents.category column alongside the shared media
|
||
// index. Unlike ClassifyMediaCategories (which can also add unrelated
|
||
// categories such as URL from message entities), this only ever answers "what
|
||
// kind of document is this", returning MediaCategoryNone for a non-document
|
||
// payload or for sticker/custom-emoji documents (they classify to nothing --
|
||
// see classifyDocumentCategory). Zero new classification logic: reuses
|
||
// classifyDocumentCategory exactly.
|
||
func DocumentMediaCategory(media *MessageMedia) MediaCategory {
|
||
if media == nil || media.Kind != MessageMediaKindDocument {
|
||
return MediaCategoryNone
|
||
}
|
||
c, ok := classifyDocumentCategory(media.Document)
|
||
if !ok {
|
||
return MediaCategoryNone
|
||
}
|
||
return c
|
||
}
|
||
|
||
func hasURLEntity(entities []MessageEntity) bool {
|
||
for _, e := range entities {
|
||
if e.Type == MessageEntityURL || e.Type == MessageEntityTextURL || e.Type == MessageEntityEmail {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|