Initial open source release

This commit is contained in:
A 2026-06-04 01:37:39 +08:00
commit 74992e893f
377 changed files with 118084 additions and 0 deletions

View file

@ -0,0 +1,385 @@
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)
}