385 lines
11 KiB
Go
385 lines
11 KiB
Go
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.availableReactions;docByID 由 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.stickerSet(set + 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)
|
||
}
|