owpengram-server/internal/rpc/convert_media.go
2026-06-04 01:37:39 +08:00

385 lines
11 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 rpc
import (
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
// 本文件集中 domain media 值对象 → tg.* 的转换tg.* 只在 rpc 层出现。
// 供 reaction / sticker 资源 RPC 与消息 media 共用。
// tgMessageMedia 把消息 media 快照转成 tg.MessageMediaClass空载荷回退 MessageMediaEmpty。
func tgMessageMedia(m *domain.MessageMedia) tg.MessageMediaClass {
if m.IsZero() {
return &tg.MessageMediaEmpty{}
}
switch m.Kind {
case domain.MessageMediaKindPhoto:
out := &tg.MessageMediaPhoto{Spoiler: m.Spoiler}
if m.Photo != nil {
out.Photo = tgPhoto(*m.Photo)
}
if m.TTLSeconds > 0 {
out.TTLSeconds = m.TTLSeconds
}
return out
case domain.MessageMediaKindDocument:
out := &tg.MessageMediaDocument{
Spoiler: m.Spoiler,
Nopremium: m.Nopremium,
Voice: m.Voice,
Round: m.Round,
Video: m.Video,
}
if m.Document != nil {
out.Document = tgDocument(*m.Document)
}
if m.TTLSeconds > 0 {
out.TTLSeconds = m.TTLSeconds
}
return out
default:
return &tg.MessageMediaEmpty{}
}
}
// tgChatPhoto 由 domain.Channel 反范式头像字段构造 ChatPhoto频道/群头像缩略)。
func tgChatPhoto(ch domain.Channel) tg.ChatPhotoClass {
if ch.PhotoID == 0 {
return &tg.ChatPhotoEmpty{}
}
p := &tg.ChatPhoto{PhotoID: ch.PhotoID, DCID: ch.PhotoDCID}
if len(ch.PhotoStripped) > 0 {
p.SetStrippedThumb(ch.PhotoStripped)
}
return p
}
// tgChannelChatPhotoFull 为 channelFull.chat_photo 构造完整 Photo合成 a/c 尺寸;
// getFile 按 photo:<id>:<type> 解析,忽略 access_hash故合成尺寸也可下载
func tgChannelChatPhotoFull(ch domain.Channel) tg.PhotoClass {
if ch.PhotoID == 0 {
return &tg.PhotoEmpty{}
}
photo := &tg.Photo{ID: ch.PhotoID, DCID: ch.PhotoDCID, Sizes: syntheticAvatarSizes()}
if len(ch.PhotoStripped) > 0 {
photo.Sizes = append([]tg.PhotoSizeClass{&tg.PhotoStrippedSize{Type: "i", Bytes: ch.PhotoStripped}}, photo.Sizes...)
}
return photo
}
func syntheticAvatarSizes() []tg.PhotoSizeClass {
return []tg.PhotoSizeClass{
&tg.PhotoSize{Type: "a", W: 160, H: 160, Size: 0},
&tg.PhotoSize{Type: "c", W: 640, H: 640, Size: 0},
}
}
// tgPhoto 把 domain.Photo 转成 tg.PhotoClass。
func tgPhoto(p domain.Photo) tg.PhotoClass {
if p.ID == 0 {
return &tg.PhotoEmpty{}
}
return &tg.Photo{
ID: p.ID,
AccessHash: p.AccessHash,
FileReference: p.FileReference,
Date: p.Date,
Sizes: tgPhotoSizes(p.Sizes),
DCID: p.DCID,
HasStickers: p.HasStickers,
}
}
// tgDocument 把 domain.Document 转成 tg.DocumentClass。
func tgDocument(d domain.Document) tg.DocumentClass {
if d.ID == 0 {
return &tg.DocumentEmpty{}
}
return &tg.Document{
ID: d.ID,
AccessHash: d.AccessHash,
FileReference: d.FileReference,
Date: d.Date,
MimeType: d.MimeType,
Size: d.Size,
Thumbs: tgDocumentThumbs(d.Thumbs),
DCID: d.DCID,
Attributes: tgDocumentAttributes(d.Attributes),
}
}
func tgDocuments(docs []domain.Document) []tg.DocumentClass {
out := make([]tg.DocumentClass, 0, len(docs))
for _, d := range docs {
out = append(out, tgDocument(d))
}
return out
}
func tgDocumentThumbs(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
if len(sizes) == 0 {
return nil
}
out := make([]tg.PhotoSizeClass, 0, len(sizes))
for _, s := range sizes {
if s.Kind == domain.PhotoSizeKindCached && len(s.Bytes) > 0 {
size := s.Size
if size == 0 {
size = len(s.Bytes)
}
if s.Type != "" && s.W > 0 && s.H > 0 && size > 0 {
out = append(out, &tg.PhotoSize{Type: s.Type, W: s.W, H: s.H, Size: size})
}
continue
}
out = append(out, tgPhotoSize(s))
}
return compactPhotoSizeClasses(out)
}
func tgPhotoSizes(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
if len(sizes) == 0 {
return nil
}
out := make([]tg.PhotoSizeClass, 0, len(sizes))
for _, s := range sizes {
out = append(out, tgPhotoSize(s))
}
return compactPhotoSizeClasses(out)
}
func tgPhotoSize(s domain.PhotoSize) tg.PhotoSizeClass {
switch s.Kind {
case domain.PhotoSizeKindDefault:
return &tg.PhotoSize{Type: s.Type, W: s.W, H: s.H, Size: s.Size}
case domain.PhotoSizeKindStripped:
return &tg.PhotoStrippedSize{Type: s.Type, Bytes: s.Bytes}
case domain.PhotoSizeKindCached:
return &tg.PhotoCachedSize{Type: s.Type, W: s.W, H: s.H, Bytes: s.Bytes}
case domain.PhotoSizeKindPath:
return &tg.PhotoPathSize{Type: s.Type, Bytes: s.Bytes}
case domain.PhotoSizeKindProgressive:
return &tg.PhotoSizeProgressive{Type: s.Type, W: s.W, H: s.H, Sizes: s.Sizes}
default:
return nil
}
}
func compactPhotoSizeClasses(in []tg.PhotoSizeClass) []tg.PhotoSizeClass {
out := in[:0]
for _, s := range in {
if s != nil {
out = append(out, s)
}
}
return out
}
func tgDocumentAttributes(attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass {
out := make([]tg.DocumentAttributeClass, 0, len(attrs))
for _, a := range attrs {
switch a.Kind {
case domain.DocAttrImageSize:
out = append(out, &tg.DocumentAttributeImageSize{W: a.W, H: a.H})
case domain.DocAttrAnimated:
out = append(out, &tg.DocumentAttributeAnimated{})
case domain.DocAttrSticker:
out = append(out, &tg.DocumentAttributeSticker{
Mask: a.Mask,
Alt: a.Alt,
Stickerset: tgInputStickerSetFromIDs(a.StickerSetID, a.StickerSetAccessHash),
})
case domain.DocAttrVideo:
out = append(out, &tg.DocumentAttributeVideo{
RoundMessage: a.RoundMessage,
SupportsStreaming: a.SupportsStreaming,
Duration: a.Duration,
W: a.W,
H: a.H,
})
case domain.DocAttrAudio:
attr := &tg.DocumentAttributeAudio{
Voice: a.Voice,
Duration: a.AudioDuration,
Title: a.Title,
Performer: a.Performer,
}
if len(a.Waveform) > 0 {
attr.SetWaveform(a.Waveform)
}
out = append(out, attr)
case domain.DocAttrFilename:
out = append(out, &tg.DocumentAttributeFilename{FileName: a.FileName})
case domain.DocAttrCustomEmoji:
out = append(out, &tg.DocumentAttributeCustomEmoji{
Free: a.Free,
TextColor: a.TextColor,
Alt: a.Alt,
Stickerset: tgInputStickerSetFromIDs(a.StickerSetID, a.StickerSetAccessHash),
})
}
}
return out
}
func tgInputStickerSetFromIDs(id, accessHash int64) tg.InputStickerSetClass {
if id == 0 {
return &tg.InputStickerSetEmpty{}
}
return &tg.InputStickerSetID{ID: id, AccessHash: accessHash}
}
// ---- available reactions ----
// tgAvailableReactions 用真实文档构造 messages.availableReactionsdocByID 由 handler 预加载。
func tgAvailableReactions(reactions []domain.AvailableReaction, docByID map[int64]domain.Document, hash int) *tg.MessagesAvailableReactions {
out := &tg.MessagesAvailableReactions{Hash: hash, Reactions: make([]tg.AvailableReaction, 0, len(reactions))}
doc := func(id int64) tg.DocumentClass {
if d, ok := docByID[id]; ok {
return tgDocument(d)
}
return &tg.DocumentEmpty{ID: id}
}
for _, r := range reactions {
ar := tg.AvailableReaction{
Inactive: r.Inactive,
Premium: r.Premium,
Reaction: r.Reaction,
Title: r.Title,
StaticIcon: doc(r.StaticIconID),
AppearAnimation: doc(r.AppearAnimationID),
SelectAnimation: doc(r.SelectAnimationID),
ActivateAnimation: doc(r.ActivateAnimationID),
EffectAnimation: doc(r.EffectAnimationID),
}
if r.AroundAnimationID != 0 {
ar.SetAroundAnimation(doc(r.AroundAnimationID))
}
if r.CenterIconID != 0 {
ar.SetCenterIcon(doc(r.CenterIconID))
}
out.Reactions = append(out.Reactions, ar)
}
return out
}
// reactionDocumentIDs 收集一组 reaction 引用的全部文档 id用于批量预加载
func reactionDocumentIDs(reactions []domain.AvailableReaction) []int64 {
seen := make(map[int64]struct{})
out := make([]int64, 0, len(reactions)*7)
for _, r := range reactions {
for _, id := range r.DocumentIDs() {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
}
return out
}
// ---- sticker sets ----
func tgStickerSet(set domain.StickerSet) tg.StickerSet {
out := tg.StickerSet{
Archived: set.Archived,
Official: set.Official,
Masks: set.Masks,
Emojis: set.Emojis,
ID: set.ID,
AccessHash: set.AccessHash,
Title: set.Title,
ShortName: set.ShortName,
Count: set.Count,
Hash: set.Hash,
}
if set.Installed {
date := set.InstalledDate
if date == 0 {
date = 1
}
out.SetInstalledDate(date)
}
if thumbs := tgStickerSetThumbs(set.Thumbs); len(thumbs) > 0 {
out.SetThumbs(thumbs)
out.SetThumbDCID(set.ThumbDCID)
out.SetThumbVersion(set.ThumbVersion)
}
if set.ThumbDocumentID != 0 {
out.SetThumbDocumentID(set.ThumbDocumentID)
}
return out
}
func tgStickerSetThumbs(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
if len(sizes) == 0 {
return nil
}
filtered := make([]domain.PhotoSize, 0, len(sizes))
for _, s := range sizes {
if s.Downloadable() {
continue
}
filtered = append(filtered, s)
}
return tgPhotoSizes(filtered)
}
func tgStickerSets(sets []domain.StickerSet) []tg.StickerSet {
out := make([]tg.StickerSet, 0, len(sets))
for _, s := range sets {
out = append(out, tgStickerSet(s))
}
return out
}
func tgStickerPacks(packs []domain.StickerPack) []tg.StickerPack {
out := make([]tg.StickerPack, 0, len(packs))
for _, p := range packs {
out = append(out, tg.StickerPack{Emoticon: p.Emoticon, Documents: append([]int64(nil), p.DocumentIDs...)})
}
return out
}
// tgMessagesStickerSet 构造完整 messages.stickerSetset + packs + documents
func tgMessagesStickerSet(set domain.StickerSet, docs []domain.Document) *tg.MessagesStickerSet {
return &tg.MessagesStickerSet{
Set: tgStickerSet(set),
Packs: tgStickerPacks(set.Packs),
Keywords: []tg.StickerKeyword{},
Documents: tgDocuments(docs),
}
}
// stickerSetRefFromInput 把 tg.InputStickerSet 转成 domain.StickerSetRef。
func stickerSetRefFromInput(input tg.InputStickerSetClass) (domain.StickerSetRef, bool) {
switch in := input.(type) {
case *tg.InputStickerSetID:
return domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: in.ID, AccessHash: in.AccessHash}, true
case *tg.InputStickerSetShortName:
return domain.StickerSetRef{Kind: domain.StickerSetRefByShortName, ShortName: in.ShortName}, true
case *tg.InputStickerSetAnimatedEmoji:
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "animated_emoji"}, true
case *tg.InputStickerSetAnimatedEmojiAnimations:
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "animated_emoji_animations"}, true
case *tg.InputStickerSetEmojiGenericAnimations:
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "emoji_generic_animations"}, true
case *tg.InputStickerSetDice:
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "dice:" + in.Emoticon}, true
default:
return domain.StickerSetRef{}, false
}
}
// mediaCatalogHash 用一组 int64文档/集合 id算稳定 hash供 *NotModified 缓存判定。
func mediaCatalogHash(values []int64) int64 {
var hash uint64
for _, v := range values {
hash ^= uint64(v)
hash = hash*0x4f25 + uint64(v)
}
return int64(hash & 0x7fffffffffffffff)
}