owpengram-server/internal/domain/media.go
A 8ff3343ae0 business: add privacy-aware user projection
(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
2026-06-14 00:19:59 +08:00

341 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package domain
// 本文件定义媒体相关的业务值对象(文档、照片、贴纸集、可用 reaction、消息媒体
// 这些类型完全不依赖 tg.*rpc 层负责 domain↔tg 转换。
//
// 字段带 json tag 是为了 store 层可直接 json.Marshal 落 JSONB消息 media 快照、
// 文档/照片元数据)。它们是协议无关的纯数据,不是 tg 生成类型。
// MediaBackend 标识 blob 字节实际存放后端。第一阶段只有本地磁盘。
type MediaBackend string
const (
// MediaBackendLocalFS 表示 blob 字节存在本地磁盘object_key 为相对路径)。
MediaBackendLocalFS MediaBackend = "localfs"
)
// FileBlob 是一个可下载的二进制对象的索引项location_key → 后端/对象键/大小/mime。
// 真正的字节由 blob backend 按 ObjectKey 读写;本结构只描述定位与元数据。
type FileBlob struct {
// LocationKey 是稳定的逻辑定位键,由 getFile 的 InputFileLocation 推导:
// doc:<id> 文档主体
// doc:<id>:<type> 文档缩略图PhotoSize type
// photo:<id>:<type> 照片某尺寸
LocationKey string `json:"location_key"`
Backend MediaBackend `json:"backend"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
SHA256 []byte `json:"sha256,omitempty"`
MimeType string `json:"mime_type,omitempty"`
}
// UploadPart 是 upload.saveFilePart/saveBigFilePart 累积的一个分片(落 PG组装后清理
type UploadPart struct {
OwnerUserID int64
FileID int64
Part int
TotalParts int // big file 已知总数small file 为 0
Big bool
Bytes []byte
}
// UploadedFileRef 引用一个客户端已通过 upload.saveFilePart(Big) 上传完毕的文件。
// rpc 层从 tg.InputFile/InputFileBig 转换得到files 服务据此组装 blob。
type UploadedFileRef struct {
OwnerUserID int64
FileID int64
Parts int
Name string
Big bool
MD5 string // small file 客户端 md5_checksumhex可校验big file 为空
}
// DocumentSpec 描述从上传文件创建 Document 的元数据(来自 InputMediaUploadedDocument
type DocumentSpec struct {
MimeType string
Attributes []DocumentAttribute
Thumb *UploadedFileRef // 可选缩略图上传,生成 doc:<id>:m
ForceFile bool
}
// FileDownloadRequest 是 upload.getFile 解析后的下载请求;
// LocationKey 由 rpc 层从 tg.InputFileLocation 推导doc:<id> / photo:<id>:<type> 等)。
type FileDownloadRequest struct {
LocationKey string
Offset int64
Limit int
}
// FileChunk 是 upload.getFile 返回的一段内容。
type FileChunk struct {
Bytes []byte
MimeType string
Total int64
}
// PhotoSizeKind 标识 PhotoSize 的 TL 变体。
type PhotoSizeKind string
const (
// PhotoSizeKindDefault → photoSize可下载type/w/h/size
PhotoSizeKindDefault PhotoSizeKind = "size"
// PhotoSizeKindStripped → photoStrippedSize内联字节秒开模糊图
PhotoSizeKindStripped PhotoSizeKind = "stripped"
// PhotoSizeKindCached → photoCachedSize内联字节 + w/h
PhotoSizeKindCached PhotoSizeKind = "cached"
// PhotoSizeKindPath → photoPathSize内联 svg path 字节,矢量占位)。
PhotoSizeKindPath PhotoSizeKind = "path"
// PhotoSizeKindProgressive → photoSizeProgressive渐进式 jpeg 多段大小)。
PhotoSizeKindProgressive PhotoSizeKind = "progressive"
)
// PhotoSize 描述照片/缩略图的一种渲染尺寸。
type PhotoSize struct {
Kind PhotoSizeKind `json:"kind"`
Type string `json:"type"`
W int `json:"w,omitempty"`
H int `json:"h,omitempty"`
Size int `json:"size,omitempty"`
Bytes []byte `json:"bytes,omitempty"` // stripped/cached/path 内联内容
Sizes []int `json:"sizes,omitempty"` // progressive
}
// Downloadable 表示该尺寸需要客户端通过 upload.getFile 拉取(而非内联字节)。
func (s PhotoSize) Downloadable() bool {
return s.Kind == PhotoSizeKindDefault || s.Kind == PhotoSizeKindProgressive
}
// DocumentAttributeKind 标识 TL DocumentAttribute 变体。
type DocumentAttributeKind string
const (
DocAttrImageSize DocumentAttributeKind = "image_size"
DocAttrAnimated DocumentAttributeKind = "animated"
DocAttrSticker DocumentAttributeKind = "sticker"
DocAttrVideo DocumentAttributeKind = "video"
DocAttrAudio DocumentAttributeKind = "audio"
DocAttrFilename DocumentAttributeKind = "filename"
DocAttrCustomEmoji DocumentAttributeKind = "custom_emoji"
)
// DocumentAttribute 是主路径用到的 TL DocumentAttribute 变体的并集。
type DocumentAttribute struct {
Kind DocumentAttributeKind `json:"kind"`
// image_size / video / sticker box
W int `json:"w,omitempty"`
H int `json:"h,omitempty"`
// sticker / custom_emoji
Alt string `json:"alt,omitempty"`
Mask bool `json:"mask,omitempty"`
StickerSetID int64 `json:"sticker_set_id,omitempty"`
StickerSetAccessHash int64 `json:"sticker_set_access_hash,omitempty"`
Free bool `json:"free,omitempty"` // custom_emoji
TextColor bool `json:"text_color,omitempty"` // custom_emoji
// video
Duration float64 `json:"duration,omitempty"`
RoundMessage bool `json:"round_message,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
// audio
AudioDuration int `json:"audio_duration,omitempty"`
Voice bool `json:"voice,omitempty"`
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
Waveform []byte `json:"waveform,omitempty"`
// filename
FileName string `json:"file_name,omitempty"`
}
// Document 是已存储的 Telegram 文档贴纸、gif、文件、视频、音频、自定义 emoji……
type Document struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
FileReference []byte `json:"file_reference,omitempty"`
Date int `json:"date,omitempty"`
MimeType string `json:"mime_type,omitempty"`
Size int64 `json:"size,omitempty"`
DCID int `json:"dc_id,omitempty"`
Attributes []DocumentAttribute `json:"attributes,omitempty"`
Thumbs []PhotoSize `json:"thumbs,omitempty"`
}
// StickerSetRef 返回该文档归属的贴纸集引用(若有 sticker/custom_emoji 属性)。
func (d Document) StickerSetRef() (id, accessHash int64, ok bool) {
for _, attr := range d.Attributes {
if attr.Kind == DocAttrSticker || attr.Kind == DocAttrCustomEmoji {
if attr.StickerSetID != 0 {
return attr.StickerSetID, attr.StickerSetAccessHash, true
}
}
}
return 0, 0, false
}
// Photo 是已存储的 Telegram 照片(头像或图片消息)。
type Photo struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
FileReference []byte `json:"file_reference,omitempty"`
Date int `json:"date,omitempty"`
DCID int `json:"dc_id,omitempty"`
HasStickers bool `json:"has_stickers,omitempty"`
Sizes []PhotoSize `json:"sizes,omitempty"`
}
// MessageMediaKind 枚举消息可挂载的媒体载荷。
type MessageMediaKind string
const (
MessageMediaKindNone MessageMediaKind = ""
MessageMediaKindPhoto MessageMediaKind = "photo"
MessageMediaKindDocument MessageMediaKind = "document"
)
// MessageMedia 是一条消息媒体载荷的业务表示(落库为消息行上的 JSONB 快照)。
type MessageMedia struct {
Kind MessageMediaKind `json:"kind"`
Photo *Photo `json:"photo,omitempty"`
Document *Document `json:"document,omitempty"`
Spoiler bool `json:"spoiler,omitempty"`
TTLSeconds int `json:"ttl_seconds,omitempty"`
Nopremium bool `json:"nopremium,omitempty"`
Voice bool `json:"voice,omitempty"`
Round bool `json:"round,omitempty"`
Video bool `json:"video,omitempty"`
}
// IsZero 表示无媒体(用于落库时跳过空快照、转换时回退 MessageMediaEmpty
func (m *MessageMedia) IsZero() bool {
return m == nil || m.Kind == MessageMediaKindNone
}
// StickerPack 是 emoji→文档 id 的映射条目messages.stickerSet.packs
type StickerPack struct {
Emoticon string `json:"emoticon"`
DocumentIDs []int64 `json:"document_ids"`
}
// StickerSetKind 区分贴纸集用途(影响 getAllStickers / getEmojiStickers 归类)。
type StickerSetKind string
const (
StickerSetKindStickers StickerSetKind = "stickers"
StickerSetKindEmoji StickerSetKind = "emoji"
StickerSetKindMasks StickerSetKind = "masks"
// StickerSetKindSystem 是 TDesktop 通过 InputStickerSetDice/AnimatedEmoji 等系统集请求的内置集。
StickerSetKindSystem StickerSetKind = "system"
)
// StickerSet 是贴纸/自定义 emoji 集的元数据 + 有序文档 id。
type StickerSet struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
ShortName string `json:"short_name"`
Title string `json:"title"`
Count int `json:"count"`
Hash int `json:"hash"`
Kind StickerSetKind `json:"set_kind"`
Official bool `json:"official,omitempty"`
Animated bool `json:"animated,omitempty"`
Videos bool `json:"videos,omitempty"`
Emojis bool `json:"emojis,omitempty"`
Masks bool `json:"masks,omitempty"`
Installed bool `json:"installed,omitempty"`
Archived bool `json:"archived,omitempty"`
InstalledDate int `json:"installed_date,omitempty"`
ThumbDocumentID int64 `json:"thumb_document_id,omitempty"`
Thumbs []PhotoSize `json:"thumbs,omitempty"`
ThumbDCID int `json:"thumb_dc_id,omitempty"`
ThumbVersion int `json:"thumb_version,omitempty"`
DocumentIDs []int64 `json:"document_ids,omitempty"`
Packs []StickerPack `json:"packs,omitempty"`
SortOrder int `json:"sort_order,omitempty"`
// SystemKey 是 TDesktop 系统集的稳定标识(如 "animated_emoji"、"dice:🎲"),用于 InputStickerSet* 路由。
SystemKey string `json:"system_key,omitempty"`
}
// ProfilePhotoKind distinguishes a user's real profile photo from the fallback
// public photo shown when privacy hides the real one.
type ProfilePhotoKind string
const (
ProfilePhotoKindProfile ProfilePhotoKind = "profile"
ProfilePhotoKindFallback ProfilePhotoKind = "fallback"
)
// ProfilePhotoRef 是渲染头像所需的最小信息(当前 profile/fallback/personal photo
type ProfilePhotoRef struct {
PhotoID int64
DCID int
Stripped []byte // photoStrippedSize 内联缩略图,可空
Personal bool // true 表示 viewer 私有联系人头像
}
// StrippedFromSizes 从照片尺寸列表里取出 stripped 缩略图字节(用于 UserProfilePhoto/ChatPhoto 占位)。
func StrippedFromSizes(sizes []PhotoSize) []byte {
for _, s := range sizes {
if s.Kind == PhotoSizeKindStripped {
return s.Bytes
}
}
return nil
}
// StickerSetRefKind 标识 InputStickerSet 的解析方式。
type StickerSetRefKind string
const (
StickerSetRefByID StickerSetRefKind = "id"
StickerSetRefByShortName StickerSetRefKind = "short_name"
StickerSetRefBySystem StickerSetRefKind = "system"
)
// StickerSetRef 是 rpc 层从 tg.InputStickerSet 转换得到的贴纸集引用。
type StickerSetRef struct {
Kind StickerSetRefKind
ID int64
AccessHash int64
ShortName string
SystemKey string
}
// AvailableReaction 描述 messages.getAvailableReactions 的一项(真实资源由文档 id 引用)。
type AvailableReaction struct {
Reaction string `json:"reaction"`
Title string `json:"title"`
Inactive bool `json:"inactive,omitempty"`
Premium bool `json:"premium,omitempty"`
StaticIconID int64 `json:"static_icon_id,omitempty"`
AppearAnimationID int64 `json:"appear_animation_id,omitempty"`
SelectAnimationID int64 `json:"select_animation_id,omitempty"`
ActivateAnimationID int64 `json:"activate_animation_id,omitempty"`
EffectAnimationID int64 `json:"effect_animation_id,omitempty"`
AroundAnimationID int64 `json:"around_animation_id,omitempty"`
CenterIconID int64 `json:"center_icon_id,omitempty"`
Order int `json:"order,omitempty"`
}
// DocumentIDs 收集该 reaction 引用的全部文档 id去零去重便于批量加载
func (r AvailableReaction) DocumentIDs() []int64 {
raw := []int64{
r.StaticIconID, r.AppearAnimationID, r.SelectAnimationID,
r.ActivateAnimationID, r.EffectAnimationID, r.AroundAnimationID, r.CenterIconID,
}
out := make([]int64, 0, len(raw))
seen := make(map[int64]struct{}, len(raw))
for _, id := range raw {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}